diff --git a/Exploration Queries/ExplorationQueryTemplate.yaml b/Exploration Queries/ExplorationQueryTemplate.yaml index ff9e121b5c..690744a25d 100644 --- a/Exploration Queries/ExplorationQueryTemplate.yaml +++ b/Exploration Queries/ExplorationQueryTemplate.yaml @@ -1,4 +1,4 @@ -Id: guid +Id: guid DisplayName: string Description: string InputEntityType: string diff --git a/Exploration Queries/InputEntity_Account/Acc2Host_HostWithMostFails.yaml b/Exploration Queries/InputEntity_Account/Acc2Host_HostWithMostFails.yaml index dfa9bb91ec..d79a3919c0 100644 --- a/Exploration Queries/InputEntity_Account/Acc2Host_HostWithMostFails.yaml +++ b/Exploration Queries/InputEntity_Account/Acc2Host_HostWithMostFails.yaml @@ -11,7 +11,7 @@ QueryPeriodBefore: 1d QueryPeriodAfter: 1d DataSources: - SecurityEvent -Tactics: +Tactics: - Persistence - Discovery - LateralMovement @@ -37,14 +37,14 @@ query: | | extend p_Account_NTDomain = case( v_Account_NTDomain has '\\', tostring(split(v_Account_UPNSuffix, '\\')[0]), v_Account_NTDomain - ) + ) // parse Account sections | extend Account_UPNSuffix = iff(Account has '@', tostring(split(Account,'@')[1]),'') | extend Account_NTDomain = iff(Account has '\\', tostring(split(Account,'\\')[0]),'') | extend Account_Name = extract(@'^([^\\]*\\)?([^@]+)@?',2,Account) // filter by account: Name has to match, NTDomain and UPNSuffix should not be different - | where ( (isnotempty(Account_Name) and Account_Name==p_Account_Name) - and + | where ( (isnotempty(Account_Name) and Account_Name==p_Account_Name) + and iff(isnotempty(p_Account_NTDomain) and isnotempty(Account_NTDomain) ,p_Account_NTDomain==Account_NTDomain,true ) and iff(isnotempty(p_Account_UPNSuffix) and isnotempty(Account_UPNSuffix) ,p_Account_UPNSuffix==Account_UPNSuffix,true ) @@ -53,8 +53,8 @@ query: | by Computer, Account | top 10 by Host_Aux_FailedLoginsCount | parse Computer with Host_NTDomain '\\' * - | extend Host_HostName = tostring(split(Computer,'.')[0]), + | extend Host_HostName = tostring(split(Computer,'.')[0]), Host_DnsDomain = strcat_array(array_slice(split(Computer,'.'),1,256),'.') - | project-away Computer, Account + | project-away Computer, Account }; MostFailedLogins('','','') diff --git a/Exploration Queries/InputEntity_Account/LeastPrevProcess_ByAccount.yaml b/Exploration Queries/InputEntity_Account/LeastPrevProcess_ByAccount.yaml index 3df0cef8ea..dea945b533 100644 --- a/Exploration Queries/InputEntity_Account/LeastPrevProcess_ByAccount.yaml +++ b/Exploration Queries/InputEntity_Account/LeastPrevProcess_ByAccount.yaml @@ -28,7 +28,7 @@ query: | | where SyslogMessage has v_Account_Name | extend info = pack('HostName', HostName, 'HostIP', HostIP) | summarize Process_Aux_StartTime=min(EventTime), Process_Aux_EndTime=max(EventTime), count(), Process_Aux_info = makeset(info) by Computer, ProcessName, ProcessID - | top 10 by count_ asc nulls last + | top 10 by count_ asc nulls last | project Process_Aux_StartTime, Process_Aux_EndTime, Process_Host_UnstructuredName=Computer, Process_ImageFile_FullPath=ProcessName, Process_ProcessId=ProcessID, Process_Aux_info }; // change value below diff --git a/Exploration Queries/InputEntity_Account/ServiceCreatedByAccount.yaml b/Exploration Queries/InputEntity_Account/ServiceCreatedByAccount.yaml index dc90562c83..28b345f31c 100644 --- a/Exploration Queries/InputEntity_Account/ServiceCreatedByAccount.yaml +++ b/Exploration Queries/InputEntity_Account/ServiceCreatedByAccount.yaml @@ -44,7 +44,7 @@ query: | | extend ServiceAccount = tostring(EventDataParse.DataItem.EventData.Data[4]['#text']) | where ImagePath !has '\\ProgramData\\Microsoft\\Windows Defender\\Definition Updates\\' | extend Process_Aux_Account_info = pack('ServiceName', ServiceName, 'ServiceType', ServiceType, 'StartType', StartType, 'ServiceAccount', ServiceAccount) - | summarize Process_Host_Aux_StartTimeUtc = min(TimeGenerated), Process_Host_Aux_EndTimeUtc = max(TimeGenerated) by Process_Host_UnstructuredName = Computer, Process_Account_Name, + | summarize Process_Host_Aux_StartTimeUtc = min(TimeGenerated), Process_Host_Aux_EndTimeUtc = max(TimeGenerated) by Process_Host_UnstructuredName = Computer, Process_Account_Name, Process_Account_NTDomain, Process_Account_UnstructuredName = UserName, Process_ImageFile_FullPath = ImagePath, tostring(Process_Aux_Account_info) | top 10 by Process_Host_Aux_StartTimeUtc desc nulls last }; diff --git a/Exploration Queries/InputEntity_Account/UserAccount_LogonsFromIPAddress.yaml b/Exploration Queries/InputEntity_Account/UserAccount_LogonsFromIPAddress.yaml index 13818d7651..383ec2deca 100644 --- a/Exploration Queries/InputEntity_Account/UserAccount_LogonsFromIPAddress.yaml +++ b/Exploration Queries/InputEntity_Account/UserAccount_LogonsFromIPAddress.yaml @@ -29,7 +29,7 @@ query: | | summarize min(TimeGenerated), max(TimeGenerated), IP_Aux_info = makeset(info) by ClientIP | project IP_Aux_StartTime = min_TimeGenerated, IP_Aux_EndTime = max_TimeGenerated, ClientIP, IP_Aux_info | project-rename IP_Address=ClientIP - | top 10 by IP_Aux_StartTime desc nulls last + | top 10 by IP_Aux_StartTime desc nulls last }; // change value below GetAllIPbyAccount ('') diff --git a/Exploration Queries/InputEntity_Account/UserAccount_ResourceLogon.yaml b/Exploration Queries/InputEntity_Account/UserAccount_ResourceLogon.yaml index b4fe41a59d..ee71e73ac9 100644 --- a/Exploration Queries/InputEntity_Account/UserAccount_ResourceLogon.yaml +++ b/Exploration Queries/InputEntity_Account/UserAccount_ResourceLogon.yaml @@ -14,7 +14,7 @@ Tactics: - Persistence - Discovery - LateralMovement - - Collection + - Collection query: | let GetAllHostsbyAccount = (v_Account_Name:string){ @@ -32,7 +32,7 @@ query: | | extend info = pack('UserDisplayName', UserDisplayName, 'UserPrincipalName', UserPrincipalName, 'AppDisplayName', AppDisplayName, 'ClientAppUsed', ClientAppUsed, 'Browser', tostring(Browser), 'IPAddress', IPAddress, 'ResultType', ResultType, 'ResultDescription', ResultDescription, 'Location', Location, 'State', State, 'City', City, 'StatusCode', StatusCode, 'StatusDetails', StatusDetails) | summarize min(TimeGenerated), max(TimeGenerated), Host_Aux_info = makeset(info) by RemoteHost , tostring(OS) | project min_TimeGenerated, max_TimeGenerated, RemoteHost, OS, Host_Aux_info - | top 10 by min_TimeGenerated desc nulls last + | top 10 by min_TimeGenerated desc nulls last | project-rename Host_UnstructuredName=RemoteHost, Host_OSVersion=OS, Host_Aux_StartTime=min_TimeGenerated, Host_Aux_EndTime=max_TimeGenerated }; // change value below diff --git a/Exploration Queries/InputEntity_Host/Host2Acc_PossibleSuccessfulBruteForce.yaml b/Exploration Queries/InputEntity_Host/Host2Acc_PossibleSuccessfulBruteForce.yaml index 124fd96cc0..368f93d2c8 100644 --- a/Exploration Queries/InputEntity_Host/Host2Acc_PossibleSuccessfulBruteForce.yaml +++ b/Exploration Queries/InputEntity_Host/Host2Acc_PossibleSuccessfulBruteForce.yaml @@ -13,7 +13,7 @@ DataSources: Tactics: - LateralMovement - CredentialAccess -query: | +query: | let BRUTEFORCE_THRESHOLD = 10; let SuccessfulLoginEventId = 4624; @@ -30,13 +30,13 @@ query: | | where p_Host_HostName=~Host_HostName and (isempty(p_Host_DnsDomain) or isempty(Host_DnsDomain) or p_Host_DnsDomain=~Host_DnsDomain) | extend Fails = (EventID == FailedLoginEventId), Success = (EventID == SuccessfulLoginEventId) | extend Account = tolower(Account) - | summarize Account_Aux_SuccessPerMin = countif(Success), Account_Aux_FailPerMin = countif(Fails) by Account, bin(TimeGenerated, 1m) + | summarize Account_Aux_SuccessPerMin = countif(Success), Account_Aux_FailPerMin = countif(Fails) by Account, bin(TimeGenerated, 1m) | where Account_Aux_FailPerMin > BRUTEFORCE_THRESHOLD and Account_Aux_SuccessPerMin > 0 | extend EventData = pack('FailPerMin',Account_Aux_FailPerMin, 'SuccessPerMin', Account_Aux_SuccessPerMin, 'Time', TimeGenerated ) | summarize Max = max(Account_Aux_FailPerMin), Account_Aux_EventsData=makeset(EventData) by Account | top 10 by Max | parse Account with Account_NTDomain '\\' * - | extend Account_Name = extract(@'^([^\\]*\\)?([^@]+)(@.*)?$',2,Account), + | extend Account_Name = extract(@'^([^\\]*\\)?([^@]+)(@.*)?$',2,Account), Account_UPNSuffix = extract(@'^([^\\]*\\)?([^@]+)(@(.*))?$',4,Account) | project Account_Name, Account_NTDomain, Account_UPNSuffix, Account_Aux_EventsData }; diff --git a/Exploration Queries/InputEntity_Host/LeastPrevIn_ByHost.yaml b/Exploration Queries/InputEntity_Host/LeastPrevIn_ByHost.yaml index 09c0fc855d..5a5798bc00 100644 --- a/Exploration Queries/InputEntity_Host/LeastPrevIn_ByHost.yaml +++ b/Exploration Queries/InputEntity_Host/LeastPrevIn_ByHost.yaml @@ -20,7 +20,7 @@ query: | let GetWireDataInboundWithHost = (v_Host_HostName:string){ WireData - | where Direction == 'Inbound' + | where Direction == 'Inbound' | where Computer has v_Host_HostName | extend info = pack('Computer', Computer, 'LocalPortNumber', LocalPortNumber, 'RemoteIP', RemoteIP, 'Direction', Direction, 'ApplicationProtocol', ApplicationProtocol) | summarize Process_Aux_Min_SessionStartTime=min(SessionStartTime), count(), IP_Aux_info = makeset(info) by ProcessName , LocalIP, ProcessID diff --git a/Exploration Queries/InputEntity_Host/LeastPrevOut_ByHost.yaml b/Exploration Queries/InputEntity_Host/LeastPrevOut_ByHost.yaml index 4c114ebb5e..b35e240b8a 100644 --- a/Exploration Queries/InputEntity_Host/LeastPrevOut_ByHost.yaml +++ b/Exploration Queries/InputEntity_Host/LeastPrevOut_ByHost.yaml @@ -20,7 +20,7 @@ query: | let GetWireDataOutboundWithHost = (v_Host_HostName:string){ WireData - | where Direction == 'Outbound' + | where Direction == 'Outbound' | where Computer has v_Host_HostName | extend info = pack('Computer', Computer, 'LocalIP', LocalIP, 'LocalPortNumber', LocalPortNumber, 'Direction', Direction, 'ApplicationProtocol', ApplicationProtocol) | summarize Process_Aux_Min_SessionStartTime=min(SessionStartTime), count(), IP_Aux_info = makeset(info) by ProcessName, RemoteIP, ProcessID diff --git a/Exploration Queries/InputEntity_Host/LeastPrevProcess_ByHost.yaml b/Exploration Queries/InputEntity_Host/LeastPrevProcess_ByHost.yaml index 3558205099..1e0d05bb08 100644 --- a/Exploration Queries/InputEntity_Host/LeastPrevProcess_ByHost.yaml +++ b/Exploration Queries/InputEntity_Host/LeastPrevProcess_ByHost.yaml @@ -23,7 +23,7 @@ query: | | where Computer has v_Host_HostName | extend info = pack('HostName', HostName, 'HostIP', HostIP) | summarize Process_Aux_StartTime=min(EventTime), Process_Aux_EndTime=max(EventTime), count(), Process_Aux_info = makeset(info) by Computer, ProcessName, ProcessID - | top 10 by count_ asc nulls last + | top 10 by count_ asc nulls last | project Process_Aux_StartTime, Process_Aux_EndTime, Process_Host_UnstructuredName=Computer, Process_ProcessId=ProcessID, Process_ImageFile_FullPath=ProcessName, Process_Aux_info }; // change value below diff --git a/Exploration Queries/InputEntity_Host/ParentProcessesOnHost.yaml b/Exploration Queries/InputEntity_Host/ParentProcessesOnHost.yaml index fc228611c1..cde1e0f41b 100644 --- a/Exploration Queries/InputEntity_Host/ParentProcessesOnHost.yaml +++ b/Exploration Queries/InputEntity_Host/ParentProcessesOnHost.yaml @@ -18,8 +18,8 @@ Tactics: query: | let GetParentProcessesOnHost = (v_Host_HostName:string){ - SecurityEvent - | where EventID == 4688 + SecurityEvent + | where EventID == 4688 | where isnotempty(ParentProcessName) // excluding well known processes, feel free to add more specific to the environment | where NewProcessName !contains ':\\Windows\\System32\\conhost.exe' and ParentProcessName !contains ':\\Windows\\System32\\conhost.exe' diff --git a/Exploration Queries/InputEntity_Host/ProcessesOnHost.yaml b/Exploration Queries/InputEntity_Host/ProcessesOnHost.yaml index d40a1da2e3..e3956ee5d9 100644 --- a/Exploration Queries/InputEntity_Host/ProcessesOnHost.yaml +++ b/Exploration Queries/InputEntity_Host/ProcessesOnHost.yaml @@ -18,7 +18,7 @@ Tactics: query: | let GetActiveProcessesOnHost = (v_Host_HostName:string){ - SecurityEvent + SecurityEvent | where EventID == 4688 // excluding well known processes, feel free to add more specific to the environment | where NewProcessName !contains ':\\Windows\\System32\\conhost.exe' and ParentProcessName !contains ':\\Windows\\System32\\conhost.exe' diff --git a/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityLeast.yaml b/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityLeast.yaml index 483d392a7b..ffce638b3c 100644 --- a/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityLeast.yaml +++ b/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityLeast.yaml @@ -19,13 +19,13 @@ query: | let AccountActivity_byIP = (v_IP_Address:string){ AzureActivity | where Caller != '' and CallerIpAddress =~ v_IP_Address - | summarize Account_Aux_StartTime = min(TimeGenerated), - Account_Aux_EndTime = max(TimeGenerated), - Count = count() by + | summarize Account_Aux_StartTime = min(TimeGenerated), + Account_Aux_EndTime = max(TimeGenerated), + Count = count() by Caller, TenantId - | top 10 by Count asc nulls last + | top 10 by Count asc nulls last | extend UPN = iff(Caller contains '@', Caller, ''), Account_AadUserId = iff(Caller !contains '@', Caller,'') | extend Account_Name = split(UPN,'@')[0] , Account_UPNSuffix = split(UPN,'@')[1] - | project Account_Name, Account_UPNSuffix, Account_AadUserId, Account_AadTenantId=TenantId, Account_Aux_StartTime , Account_Aux_EndTime + | project Account_Name, Account_UPNSuffix, Account_AadUserId, Account_AadTenantId=TenantId, Account_Aux_StartTime , Account_Aux_EndTime }; AccountActivity_byIP('
') diff --git a/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityMost.yaml b/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityMost.yaml index b3d2a8a76c..024b31cd20 100644 --- a/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityMost.yaml +++ b/Exploration Queries/InputEntity_IP/IP2Account_byAccountAzureActivityMost.yaml @@ -19,13 +19,13 @@ query: | let AccountActivity_byIP = (v_IP_Address:string){ AzureActivity | where Caller != '' and CallerIpAddress =~ v_IP_Address - | summarize Account_Aux_StartTime = min(TimeGenerated), - Account_Aux_EndTime = max(TimeGenerated), - Count = count() by + | summarize Account_Aux_StartTime = min(TimeGenerated), + Account_Aux_EndTime = max(TimeGenerated), + Count = count() by Caller, TenantId - | top 10 by Count desc nulls last + | top 10 by Count desc nulls last | extend UPN = iff(Caller contains '@', Caller, ''), Account_AadUserId = iff(Caller !contains '@', Caller,'') | extend Account_Name = split(UPN,'@')[0] , Account_UPNSuffix = split(UPN,'@')[1] - | project Account_Name, Account_UPNSuffix, Account_AadUserId, Account_AadTenantId=TenantId, Account_Aux_StartTime , Account_Aux_EndTime + | project Account_Name, Account_UPNSuffix, Account_AadUserId, Account_AadTenantId=TenantId, Account_Aux_StartTime , Account_Aux_EndTime }; AccountActivity_byIP('
') diff --git a/Exploration Queries/InputEntity_IP/IP2Host_HostByTrafficFromIPLeast.yaml b/Exploration Queries/InputEntity_IP/IP2Host_HostByTrafficFromIPLeast.yaml index e18c0c5ccd..5b42402900 100644 --- a/Exploration Queries/InputEntity_IP/IP2Host_HostByTrafficFromIPLeast.yaml +++ b/Exploration Queries/InputEntity_IP/IP2Host_HostByTrafficFromIPLeast.yaml @@ -19,7 +19,7 @@ query: | let HostsReceivingDatafromIP = (v_IP_Address:string){ WireData | parse Computer with HostName '.' Host_DnsDomain - | where SessionState == 'Disconnected' + | where SessionState == 'Disconnected' | where RemoteIP =~ v_IP_Address | extend Host_HostName = iff(Computer has '.', HostName, Computer) | summarize Host_Aux_BytesReceived = sum(ReceivedBytes), Host_Aux_LocalIPs=make_set(LocalIP) by Host_HostName, Host_DnsDomain diff --git a/Exploration Queries/InputEntity_IP/IP2IP_IPsWithMostDROPs.yaml b/Exploration Queries/InputEntity_IP/IP2IP_IPsWithMostDROPs.yaml index c7fc28c307..e2504d7dab 100644 --- a/Exploration Queries/InputEntity_IP/IP2IP_IPsWithMostDROPs.yaml +++ b/Exploration Queries/InputEntity_IP/IP2IP_IPsWithMostDROPs.yaml @@ -10,7 +10,7 @@ QueryPeriodBefore: 6h QueryPeriodAfter: 6h DataSources: - WindowsFirewall -Tactics: +Tactics: - Exfiltration - CommandAndControl - Collection diff --git a/Exploration Queries/InputEntity_IP/IpAddress_AccountLogons.yaml b/Exploration Queries/InputEntity_IP/IpAddress_AccountLogons.yaml index 54400d520e..e99c91c712 100644 --- a/Exploration Queries/InputEntity_IP/IpAddress_AccountLogons.yaml +++ b/Exploration Queries/InputEntity_IP/IpAddress_AccountLogons.yaml @@ -18,7 +18,7 @@ Tactics: query: | let GetAllAccountByIP = (v_IP_Address:string){ - OfficeActivity + OfficeActivity | where ClientIP =~ v_IP_Address | extend info = pack('ClientIP', ClientIP, 'UserType', UserType, 'Operation', Operation, 'OfficeWorkload', OfficeWorkload, 'ResultStatus', ResultStatus) | summarize min(TimeGenerated), max(TimeGenerated), Account_Aux_Count=count(), Account_Aux_info = makeset(info) by UserId diff --git a/Exploration Queries/InputEntity_IP/LeastPrevIn_ByIPAddress.yaml b/Exploration Queries/InputEntity_IP/LeastPrevIn_ByIPAddress.yaml index d5f66d17c2..d890a0f85a 100644 --- a/Exploration Queries/InputEntity_IP/LeastPrevIn_ByIPAddress.yaml +++ b/Exploration Queries/InputEntity_IP/LeastPrevIn_ByIPAddress.yaml @@ -21,7 +21,7 @@ query: | let GetWireDataInboundWithIp = (v_IPAddress:string){ WireData - | where Direction == 'Inbound' + | where Direction == 'Inbound' | where RemoteIP has v_IPAddress | extend info = pack('LocalPortNumber', LocalPortNumber, 'RemoteIP', RemoteIP, 'Direction', Direction, 'ApplicationProtocol', ApplicationProtocol) | summarize Process_Aux_EarliestSessionStartTime=min(SessionStartTime), count(), IP_Aux_info = makeset(info) by Computer, ProcessName , LocalIP, ProcessID diff --git a/Exploration Queries/InputEntity_IP/LeastPrevOut_ByIPAddress.yaml b/Exploration Queries/InputEntity_IP/LeastPrevOut_ByIPAddress.yaml index 24bae2e550..5ee0aa7c63 100644 --- a/Exploration Queries/InputEntity_IP/LeastPrevOut_ByIPAddress.yaml +++ b/Exploration Queries/InputEntity_IP/LeastPrevOut_ByIPAddress.yaml @@ -17,7 +17,7 @@ Tactics: - Discovery - LateralMovement - Collection -query: | +query: | let GetWireDataOutboundWithIp = (v_IP_Address:string){ WireData diff --git a/Exploration Queries/InputEntity_IP/LeastPrevUsersByIPAddress.yaml b/Exploration Queries/InputEntity_IP/LeastPrevUsersByIPAddress.yaml index 837871130d..43da131e8d 100644 --- a/Exploration Queries/InputEntity_IP/LeastPrevUsersByIPAddress.yaml +++ b/Exploration Queries/InputEntity_IP/LeastPrevUsersByIPAddress.yaml @@ -26,7 +26,7 @@ query: | | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city) | extend info = pack('AppDisplayName', AppDisplayName, 'ClientAppUsed', ClientAppUsed, 'Browser', tostring(Browser), 'IPAddress', IPAddress, 'ResultType', ResultType, 'ResultDescription', ResultDescription, 'Location', Location, 'State', State, 'City', City, 'StatusCode', StatusCode, 'StatusDetails', StatusDetails) | summarize min(TimeGenerated), max(TimeGenerated), count(), Account_Aux_info = makeset(info) by RemoteHost , UserDisplayName, tostring(OS), UserPrincipalName, AADTenantId, UserId - | top 10 by count_ asc nulls last + | top 10 by count_ asc nulls last | project Account_Aux_StartTime = min_TimeGenerated, Account_Aux_EndTime = max_TimeGenerated, RemoteHost, UserDisplayName, OS, UserPrincipalName, AADTenantId, UserId, Account_Aux_info | project-rename Account_UnstructuredName=UserPrincipalName, Account_DisplayName=UserDisplayName, Account_AadTenantId=AADTenantId, Account_AadUserId=UserId, Account_Host_UnstructuredName=RemoteHost, Account_Host_OSVersion=OS }; diff --git a/Exploration Queries/InputEntity_IP/MostPrevLxHosts_ByIP.yaml b/Exploration Queries/InputEntity_IP/MostPrevLxHosts_ByIP.yaml index 84b393e1bc..0899294afb 100644 --- a/Exploration Queries/InputEntity_IP/MostPrevLxHosts_ByIP.yaml +++ b/Exploration Queries/InputEntity_IP/MostPrevLxHosts_ByIP.yaml @@ -23,7 +23,7 @@ query: | | where HostIP has v_IP_Address | extend info = pack('HostIP', HostIP, 'ProcessName', ProcessName, 'SeverityLevel', SeverityLevel) | summarize min(EventTime), max(EventTime), count(), Host_Aux_info = makeset(info) by Computer - | top 10 by count_ desc nulls last + | top 10 by count_ desc nulls last | project Host_Aux_StartTime = min_EventTime, Host_Aux_EndTime = max_EventTime, Computer, Host_Aux_info | project-rename Host_UnstructuredName=Computer }; diff --git a/Exploration Queries/InputEntity_IP/MostPrevUsersByIPAddress.yaml b/Exploration Queries/InputEntity_IP/MostPrevUsersByIPAddress.yaml index 54f03370ac..712a2b436e 100644 --- a/Exploration Queries/InputEntity_IP/MostPrevUsersByIPAddress.yaml +++ b/Exploration Queries/InputEntity_IP/MostPrevUsersByIPAddress.yaml @@ -26,7 +26,7 @@ query: | | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city) | extend info = pack('AppDisplayName', AppDisplayName, 'ClientAppUsed', ClientAppUsed, 'Browser', tostring(Browser), 'IPAddress', IPAddress, 'ResultType', ResultType, 'ResultDescription', ResultDescription, 'Location', Location, 'State', State, 'City', City, 'StatusCode', StatusCode, 'StatusDetails', StatusDetails) | summarize min(TimeGenerated), max(TimeGenerated), count(), Account_Aux_info = makeset(info) by RemoteHost , UserDisplayName, tostring(OS), UserPrincipalName, AADTenantId, UserId - | top 10 by count_ desc nulls last + | top 10 by count_ desc nulls last | project Account_Aux_StartTimeUtc = min_TimeGenerated, Account_Aux_EndTimeUtc = max_TimeGenerated, RemoteHost, UserDisplayName, OS, UserPrincipalName, AADTenantId, UserId, Account_Aux_info | project-rename Account_UnstructuredName=UserPrincipalName, Account_DisplayName=UserDisplayName, Account_AadTenantId=AADTenantId, Account_AadUserId=UserId, Account_Host_UnstructuredName=RemoteHost, Account_Host_OSVersion=OS }; diff --git a/Exploration Queries/InputEntity_Process/LeastPrevLxHosts_ByProcess.yaml b/Exploration Queries/InputEntity_Process/LeastPrevLxHosts_ByProcess.yaml index 15bc098b0a..00b3d3a026 100644 --- a/Exploration Queries/InputEntity_Process/LeastPrevLxHosts_ByProcess.yaml +++ b/Exploration Queries/InputEntity_Process/LeastPrevLxHosts_ByProcess.yaml @@ -26,7 +26,7 @@ query: | | where ProcessName has v_Process_ImageFile_FullPath | extend info = pack('HostName', HostName, 'HostIP', HostIP, 'ProcessName', ProcessName, 'SyslogMessage', SyslogMessage) | summarize min(EventTime), max(EventTime), count(), Host_Aux_info = makeset(info) by Computer - | top 10 by count_ asc nulls last + | top 10 by count_ asc nulls last | project Host_Aux_StartTime=min_EventTime, Host_Aux_EndTime=max_EventTime, Computer, Host_Aux_info | project-rename Host_UnstructuredName=Computer }; diff --git a/Exploration Queries/IoT/Process_byIoTDevice.yaml b/Exploration Queries/IoT/Process_byIoTDevice.yaml index 6b6617a05e..8a1035f222 100644 --- a/Exploration Queries/IoT/Process_byIoTDevice.yaml +++ b/Exploration Queries/IoT/Process_byIoTDevice.yaml @@ -9,12 +9,12 @@ OutputEntityTypes: - Process QueryPeriodBefore: 30m QueryPeriodAfter: 30m -DataSources: +DataSources: - SecurityIoTRawEvent query: | let Process_byIoTDevice = (v_IotDevice_DeviceId:string, v_IoTDevice_IoTHub:string){ - SecurityIoTRawEvent + SecurityIoTRawEvent | where RawEventName =~ 'ProcessCreate' | where AssociatedResourceId =~ parse_json(v_IoTDevice_IoTHub)['ResourceId'] and DeviceId =~ v_IotDevice_DeviceId | extend Process_CommandLine = tostring(parse_json(EventDetails)['CommandLine']) diff --git a/Insights/Account/AccountsActions.yaml b/Insights/Account/AccountsActions.yaml new file mode 100644 index 0000000000..283b8e368b --- /dev/null +++ b/Insights/Account/AccountsActions.yaml @@ -0,0 +1,70 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: AuditLogs + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +RequiredInputFieldsSets: + - - Account_Name + - Account_NTDomain + - - Account_Name + - Account_UPNSuffix + - - Account_AADUserId + - - Account_SID +BaseQuery: | + let GetAccountActions = (Account_Name:string, Account_NTDomain:string, Account_UPNSuffix:string, Account_AADUserId:string, Account_SID:string){ + union isfuzzy=true + ( + AuditLogs + | where tostring(bag_keys(InitiatedBy)[0]) == "user" + | where OperationName in~ ('Delete user', 'Change user password', 'Reset user password', 'Change password (self-service)', 'Reset password (by admin)', 'Reset password (self-service)', 'Update user') + | extend userPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName) + | extend userName=tostring(split(userPrincipalName, '@')[0]) + | extend userUpnSuffix=tostring(split(userPrincipalName, '@')[1]) + | extend userId=tostring(parse_json(tostring(InitiatedBy.user)).id) + | where (userName == Account_Name and userUpnSuffix == Account_UPNSuffix ) or (userId == Account_AADUserId ) + | extend Target_UPN = tostring(TargetResources[0].userPrincipalName) + | extend Target_Name = tostring(split(Target_UPN, '@')[0]) + | extend Target_UPNSuffix = tostring(split(Target_UPN, '@')[1]) + | extend Target_AADUserId = tostring(TargetResources[0].id) + | extend Action = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0]))) + | extend ModifiedProperty = parse_json(Action).displayName + | extend ModifiedValue = parse_json(Action).newValue + | extend DisableUser = iif(ModifiedProperty =~ 'AccountEnabled' and ModifiedValue =~ '[false]', 'True', 'False') + ), + ( + SecurityEvent + | where EventID in (4720, 4722, 4723, 4724, 4725, 4726, 4740) + | where AccountType =~ "user" or isempty(AccountType) + | extend userName=tostring(split(Account,"\\")[1]), userNTDomain=tostring(split(Account,"\\")[0]), userSid=SubjectUserSid + | where (Account_Name==userName and userNTDomain==Account_NTDomain) or SubjectUserSid == Account_SID + | extend OperationName = tostring(EventID) + | extend Target_Name = TargetUserName, Target_NTDomain = TargetDomainName, Target_SID = TargetSid + ) + }; + GetAccountActions('{{Account_Name}}', '{{Account_NTDomain}}', '{{Account_UPNSuffix}}', '{{Account_AADUserId}}', '{{Account_SID}}') + +Activities: + EnabledByDefault: true + Items: + - Id: d6d08c94-455f-4ea5-8f76-fc6c0c442cfa + Title: "The user has created an account" + Content: "The user has created the account {{Target_Name}} {{Count}} time(s)" + Description: "This activity displays account creation events performed by the user" + QueryDefinitions: + Filter: "where OperationName in~ ('Add user', '4720')" + SummarizeBy: "Target_Name, Target_NTDomain, Target_UPNSuffix" + - Id: e0459780-ac9d-4b72-8bd4-fecf6b46a0a1 + Title: "The user has deleted an account" + Content: "The user has deleted the account {{Target_Name}} {{Count}} time(s)" + Description: "This activity displays account deletion events performed by the user" + QueryDefinitions: + Filter: "where OperationName in~ ('Delete user', '4726')" + SummarizeBy: "Target_Name, Target_NTDomain, Target_UPNSuffix" + - Id: ad1f4269-5418-4c46-a3b6-4ec01031de60 + Title: "The user has reset an account's password" + Content: "The password for account {{Target_Name}} was reset by the user {{Count}} time(s)" + Description: "This activity displays password reset events performed by the user" + QueryDefinitions: + Filter: "where OperationName in~ ('Change user password', 'Reset user password', 'Change password (self-service)', 'Reset password (by admin)', 'Reset password (self-service)', '4724', '4723')" + SummarizeBy: "Target_Name, Target_NTDomain, Target_UPNSuffix" diff --git a/Insights/Account/ActionsOnAccounts.yaml b/Insights/Account/ActionsOnAccounts.yaml new file mode 100644 index 0000000000..eac2fe2ca8 --- /dev/null +++ b/Insights/Account/ActionsOnAccounts.yaml @@ -0,0 +1,126 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: AuditLogs + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +RequiredInputFieldsSets: + - - Account_Name + - Account_NTDomain + - - Account_Name + - Account_UPNSuffix + - - Account_AADUserId + - - Account_SID +BaseQuery: | + let GetAccountActions = (v_Account_Name:string, v_Account_NTDomain:string, v_Account_UPNSuffix:string, v_Account_AADUserId:string, v_Account_SID:string){ + AuditLogs + | where OperationName in~ ('Delete user', 'Change user password', 'Reset user password', 'Change password (self-service)', 'Reset password (by admin)', 'Reset password (self-service)', 'Update user') + | extend UserPrincipalName = tostring(TargetResources[0].userPrincipalName) + | extend Account_Name = tostring(split(UserPrincipalName, '@')[0]) + | extend Account_UPNSuffix = tostring(split(UserPrincipalName, '@')[1]) + | extend Action = tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[0]))) + | extend ModifiedProperty = parse_json(Action).displayName + | extend ModifiedValue = parse_json(Action).newValue + | extend Account_AADUserId = tostring(TargetResources[0].id) + | extend DisableUser = iif(ModifiedProperty =~ 'AccountEnabled' and ModifiedValue =~ '[false]', 'True', 'False') + | union isfuzzy=true ( + SecurityEvent + | where EventID in (4720, 4722, 4723, 4724, 4725, 4726, 4740) + | extend OperationName = tostring(EventID) + | where AccountType =~ "user" or isempty(AccountType) + | extend Account_Name = TargetUserName, Account_NTDomain = TargetDomainName, Account_SID = TargetSid + ) + | where (Account_Name =~ v_Account_Name and (Account_UPNSuffix =~ v_Account_UPNSuffix or Account_NTDomain =~ v_Account_NTDomain)) or Account_AADUserId =~ v_Account_AADUserId or Account_SID =~ v_Account_SID + }; + GetAccountActions('{{Account_Name}}', '{{Account_NTDomain}}', '{{Account_UPNSuffix}}', '{{Account_AADUserId}}', '{{Account_SID}}') +# The queries for the insights. +Insights: + Id: 6db7f5d1-f41e-46c2-b935-230b36a569e6 + DisplayName: Actions on account + Description: | + Summary of actions taken on the specified account, grouped by action: password resets and changes, account lockouts (policy or admin), account creation and deletion, account enabled and disabled + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: Action + OutputType: String + SupportDeepLink: false + - Header: Most Recent + OutputType: Date + SupportDeepLink: false + - Header: Count + OutputType: Number + SupportDeepLink: true + + QueriesDefinitions: + # Account Password Resets + - Filter: "where OperationName in~ ('Change user password', 'Reset user password', 'Change password (self-service)', 'Reset password (by admin)', 'Reset password (self-service)', '4724', '4723')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Disabled by Policy + - Filter: "where OperationName in~ ('Blocked from self-service password reset', '4740')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Disabled by Admin + - Filter: "where OperationName == '4725' or (OperationName =~ 'Update user' and DisableUser =~ 'True')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Created + - Filter: "where OperationName in~ ('Add user', '4720')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Deleted + - Filter: "where OperationName in~ ('Delete user', '4726')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Deleted or Disabled + - Filter: "where OperationName in~ ('4725', 'Blocked from self-service password reset', '4740') or (OperationName =~ 'Update user' and DisableUser =~ 'True')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Created or Enabled + - Filter: "where OperationName in~ ('4722', '4767') or (OperationName =~ 'Update user' and DisableUser =~ 'False')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + # Account Setting Changed + - Filter: "where OperationName in~ ('Update user','4738')" + Summarize: "summarize MostRecent = max(TimeGenerated), Count = count() by OperationName" + Project: "project Title = OperationName, MostRecent, Count" + LinkColumnsDefinitions: + - ProjectedName: Count + Query: "{{BaseQuery}} | {{RowFilter}}" + + ChartQuery: + Title: "Actions by type" + DataSets: + - Query: "summarize Count = count() by bin(TimeGenerated, 1h), OperationName" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "OperationName" + Type: BarChart + + AdditionalQuery: + Text: "See all account activity" + Query: "project TimeGenerated, UserPrincipalName, Account_Name, OperationName, Activity, DisableUser, TargetSid, AADUserId, InitiatedBy, AADTenantId, AccountType, Computer, SubjectAccount, SubjectUserSid, EventData" diff --git a/Insights/Account/AuditLogs_ConsentToApp.yaml b/Insights/Account/AuditLogs_ConsentToApp.yaml new file mode 100644 index 0000000000..553d56a394 --- /dev/null +++ b/Insights/Account/AuditLogs_ConsentToApp.yaml @@ -0,0 +1,32 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: AuditLogs +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +- - Account_AadUserId +BaseQuery: |- + let UserConsentToApplication = (Account_Name:string, Account_UPNSuffix:string, Account_AadUserId:string){ + let account_upn = iff(Account_Name != "" and Account_UPNSuffix != "" + , strcat(Account_Name,"@",Account_UPNSuffix) + ,"" ); + AuditLogs + | where OperationName == "Consent to application" + | extend Source_Account_UPNSuffix = tostring(todynamic(InitiatedBy) ["user"]["userPrincipalName"]), Source_Account_AadUserId = tostring(todynamic(InitiatedBy) ["user"]["id"]) + | where (account_upn != "" and account_upn =~ Source_Account_UPNSuffix) + or (Account_AadUserId != "" and Account_AadUserId =~ Source_Account_AadUserId) + | extend Target_CloudApplication_Name = tostring(todynamic(TargetResources)[0]["displayName"]), Target_CloudApplication_AppId = tostring(todynamic(TargetResources)[0]["id"]) + }; + UserConsentToApplication('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_AadUserId}}') + +Activities: + EnabledByDefault: true + Title: "The user consented to OAuth application" + Content: "The user consented to the OAuth application named {{Target_CloudApplication_Name}} {{Count}} time(s)" + Items: + - Id: 5e9ecee5-e7a4-4a2a-94c4-9c0e22e1b673 + Description: This activity lists user's consents to an OAuth applications. + QueryDefinitions: + SummarizeBy: Target_CloudApplication_AppId, Target_CloudApplication_Name diff --git a/Insights/Account/AzActivity_AzureOps.yaml b/Insights/Account/AzActivity_AzureOps.yaml new file mode 100644 index 0000000000..d8a49f0c30 --- /dev/null +++ b/Insights/Account/AzActivity_AzureOps.yaml @@ -0,0 +1,33 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: AzureActivity +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +- - Account_AadUserId +BaseQuery: |- + let AzureRunProcess = (Account_Name:string, Account_UPNSuffix:string,Account_AadUserId:string){ + let upn = strcat(Account_Name,"@",Account_UPNSuffix); + AzureActivity + | where (isnotempty(Account_AadUserId) and Caller =~ Account_AadUserId) or Caller =~ upn + | where OperationName contains "Run Command on Virtual Machine" + or (OperationName == "List Storage Account Keys" and ActivityStatus == "Succeeded") + or OperationName == "Create or Update Virtual Machine" + or OperationName == "Create Deployment" + or OperationName == "Create role assignment" + | project-rename Target_AzureResource_ResourceId = _ResourceId, Source_IP_Address = CallerIpAddress + | extend shortResourceId = tostring(split(ResourceId,'/')[-1]) + }; + AzureRunProcess('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_AadUserId}}') + +Activities: + EnabledByDefault: true + Title: "User performed operation on azure resource from IP" + Content: "User performed operation {{OperationName}} on azure resource: {{shortResourceId}} from IP {{Source_IP_Address}} {{Count}} time(s)" + Items: + - Id: cab4058a-0707-4a02-b76f-cf96270823ed + Description: This activity lists the user's activities on Azure. + QueryDefinitions: + SummarizeBy: Target_AzureResource_ResourceId, Source_IP_Address, shortResourceId, OperationName diff --git a/Insights/Account/EventLogClear.yaml b/Insights/Account/EventLogClear.yaml new file mode 100644 index 0000000000..da23891208 --- /dev/null +++ b/Insights/Account/EventLogClear.yaml @@ -0,0 +1,88 @@ +SchemaVersion: 1.0 +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: SecurityEvent + - DataType: Event +RequiredInputFieldsSets: + - - Account_Name + - Account_NTDomain + - - Account_SID +BaseQuery: | + let UserClearedEventLog = (v_Account_Name:string, v_Account_NTDomain:string, v_Account_SID:string){ + SecurityEvent + // 1102 - This event generates every time Windows Security audit log was cleared + | where EventID == 1102 and EventSourceName =~ 'Microsoft-Windows-Eventlog' + | parse EventData with * 'SubjectUserName>' SubjectUserName '<' * + | parse EventData with * 'SubjectUserSid>' SubjectUserSid '<' * + | parse EventData with * 'SubjectLogonId>' SubjectLogonId '<' * + | parse EventData with * 'SubjectDomainName>' SubjectDomainName '<' * + | parse EventData with * 'BackupPath>' BackupPath'<' * + | extend ClearedLog = Type + | extend Account_Name = SubjectUserName, Account_NTDomain = SubjectDomainName + | union isfuzzy=true + ( Event + // 104 - This event generates every time a Windows Event log was cleared + | where EventID == 104 and Source == "Microsoft-Windows-Eventlog" + | parse EventData with * 'SubjectUserName>' SubjectUserName '<' * + | parse EventData with * 'SubjectUserSid>' SubjectUserSid '<' * + | parse EventData with * 'SubjectLogonId>' SubjectLogonId '<' * + | parse EventData with * 'SubjectDomainName>' SubjectDomainName '<' * + | parse EventData with * 'BackupPath>' BackupPath'<' * + | parse RenderedDescription with * 'The' ClearedLog 'log' * + | extend Account_Name = SubjectUserName, Account_NTDomain = SubjectDomainName, Account_SID = SubjectUserSid + ) + | project TimeGenerated, Computer, EventID, Account_Name, SubjectUserName, SubjectUserSid, Account_SID, SubjectLogonId, Account_NTDomain, SubjectDomainName, SourceComputerId, _ResourceId, ClearedLog, BackupPath + | where (Account_Name =~ v_Account_Name and Account_NTDomain =~ v_Account_NTDomain) or (isnotempty(v_Account_SID) and Account_SID =~ v_Account_SID) + }; + UserClearedEventLog('{{Account_Name}}', '{{Account_NTDomain}}', '{{Account_SID}}') +# The queries for the insights. +Insights: + Id: 5a70a68d-25d4-4012-b73e-4f302a16c06a + DisplayName: Event Logs cleared by user + Description: | + Provides the datetime & count of number of times any event log was cleared by the user. + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: Cleared Log + OutputType: String + - Header: Times Cleared + OutputType: Number + - Header: Host Count + OutputType: Number + SupportDeepLink: true + - Header: Host(s) + OutputType: String + QueriesDefinitions: + # SecurityEvent cleared + - Filter: "where ClearedLog =~ 'SecurityEvent'" + Summarize: "summarize Hosts = makeset(Computer), HostCount = dcount(Computer), EventCount = count() by ClearedLog" + Project: "project Title = ClearedLog, EventCount, HostCount, Hosts = case(array_length(Hosts) == 1, tostring(Hosts[0]), array_length(Hosts) > 1, 'Many', 'None')" + LinkColumnsDefinitions: + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + # Other Event Cleared + - Filter: "where ClearedLog !~ 'SecurityEvent'" + Summarize: "summarize Hosts = makeset(Computer), HostCount = dcount(Computer), EventCount = count() by ClearedLog" + Project: "project Title = ClearedLog, EventCount, HostCount, Hosts = case(array_length(Hosts) == 1, tostring(Hosts[0]), array_length(Hosts) > 1, 'Many', 'None')" + LinkColumnsDefinitions: + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + ChartQuery: + Title: "Log clear activity over time" + DataSets: + - Query: "summarize EventCount = count() by bin(TimeGenerated, 1d) | extend Legend = 'EventCount'" + XColumnName: TimeGenerated + YColumnName: EventCount + LegendColumnName: Legend + - Query: "summarize HostCount = dcount(Computer) by bin(TimeGenerated, 1d) | extend Legend = 'HostCount'" + XColumnName: TimeGenerated + YColumnName: HostCount + LegendColumnName: Legend + Type: BarChart + AdditionalQuery: + Text: See All Log Clear Activity + Query: "project TimeGenerated, Computer, EventID, Account_Name, SubjectUserName, SubjectUserSid, SubjectLogonId, Account_NTDomain, SubjectDomainName, SourceComputerId, _ResourceId, ClearedLog, BackupPath" \ No newline at end of file diff --git a/Insights/Account/GroupAdditions.yaml b/Insights/Account/GroupAdditions.yaml new file mode 100644 index 0000000000..664f918ea6 --- /dev/null +++ b/Insights/Account/GroupAdditions.yaml @@ -0,0 +1,163 @@ +SchemaVersion: 1.0 +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: SecurityEvent +RequiredInputFieldsSets: + - - Account_Name + - Account_NTDomain + - - Account_SID +BaseQuery: | + let WellKnownLocalSID = 'S-1-5-32-5[0-9][0-9]$'; + let WellKnownGroupSID = 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-498$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1000$'; + let GetGroupAddForUser = (v_Account_Name:string, v_Account_NTDomain:string, v_Account_SID:string){ + SecurityEvent + | where EventID in (4728, 4732, 4756) + | where AccountType =~ 'User' + | extend Account_Name = case( + // Handles mixed use scenario of NTDomain\AccountName@UPNSuffix + SubjectUserName has '@' and SubjectUserName has '\\', tostring(split(tostring(split(SubjectUserName, '\\')[1]),'@')[0]), + SubjectUserName has '@', tostring(split(SubjectUserName, '@')[0]), + SubjectUserName has '\\', tostring(split(SubjectUserName, '\\')[1]), + SubjectUserName + ) + | extend Account_NTDomain = case( + SubjectDomainName has '\\', tostring(split(SubjectDomainName, '\\')[0]), + // Handles UPN scenario of AccountName@UPNSuffix to pull potential NTDomain from + SubjectDomainName has '@', tostring(split(tostring(split(SubjectDomainName, '@')[1]),'.')[0]), + SubjectDomainName + ) + | extend MemberAdded = case( MemberName has 'CN=', tostring(split(tostring(split(MemberName, ',')[0]),'CN=')[1]), MemberName == '-', MemberSid, MemberName) + | extend MemberNameMatch = iff(isnotempty(v_Account_Name) and MemberAdded has v_Account_Name, true, false) + | extend MemberNTDomainMatch = iff(isnotempty(v_Account_NTDomain) and MemberAdded has v_Account_NTDomain, true, false) + | extend MemberSidMatch = iff(isnotempty(v_Account_SID) and MemberSid =~ v_Account_SID, true, false) + | extend SubjectNameMatch = iff(isnotempty(v_Account_Name) and SubjectUserName =~ v_Account_Name, true, false) + | extend SubjectNTDomainMatch = iff(isnotempty(v_Account_NTDomain) and SubjectDomainName =~ v_Account_NTDomain, true, false) + | extend SubjectSidMatch = iff(isnotempty(v_Account_SID) and SubjectUserSid has v_Account_SID, true, false) + | where (MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true or (SubjectNameMatch == true and SubjectNTDomainMatch == true) or SubjectSidMatch == true + | project TimeGenerated, EventID, Activity, Computer, MemberName, MemberAdded, MemberSid, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, SubjectAccount, SubjectUserName, SubjectUserSid, WellKnownGroupSID, WellKnownLocalSID, + MemberNameMatch, MemberNTDomainMatch, MemberSidMatch, SubjectNameMatch, SubjectNTDomainMatch, SubjectSidMatch + | extend GroupName = TargetUserName, AddedBy = SubjectAccount + //support for Activities + | extend timestamp = TimeGenerated, AccountCustomEntity = SubjectAccount + }; + GetGroupAddForUser('{{Account_Name}}', '{{Account_NTDomain}}', '{{Account_SID}}') +# The queries for the insights. +Insights: + Id: bec9d204-c96c-4a34-8042-b49e89dbff89 + DisplayName: Group additions + Description: | + Summary of user additions to Groups for the specific user. Specifically, we provide the count of All Groups, [Privileged Groups](https://docs.microsoft.com/windows/security/identity-protection/access-control/active-directory-security-groups) and Remote Desktop Users Group. + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: "Groups" + OutputType: String + SupportDeepLink: false + - Header: GroupCount + OutputType: Number + SupportDeepLink: true + - Header: HostCount + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + + # UserAddedToPrivilegedGroups + - Filter: "where ((MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true) and (TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID)" + Summarize: "summarize GroupCount = dcount(GroupName), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'Privileged', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserAddedToRemoteDesktopGroup + - Filter: "where ((MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true) and TargetSid in ('S-1-5-32-555')" + Summarize: "summarize GroupCount = dcount(GroupName), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'Remote Desktop', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserAddedToPrivilegedGroupsExcludeRDP + - Filter: "where ((MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true) and (TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID) | where TargetSid !in ('S-1-5-32-555')" + Summarize: "summarize GroupCount = dcount(GroupName), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'Privileged(non-RDP)', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserAddedToGroups + - Filter: "where (MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true" + Summarize: "summarize GroupCount = dcount(GroupName), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'All Groups', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # GroupsAddedUsersTo + - Filter: "where (SubjectNameMatch == true and SubjectNTDomainMatch == true) or SubjectSidMatch == true" + Summarize: "summarize GroupCount = dcount(GroupName), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'Groups this user added users to', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UsersAddedToGroups + - Filter: "where (SubjectNameMatch == true and SubjectNTDomainMatch == true) or SubjectSidMatch == true " + Summarize: "summarize GroupCount = dcount(MemberAdded), HostCount = dcount(Computer) by SubjectAccount" + Project: "project Title = 'Users this user added to Groups', GroupCount, HostCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + ChartQuery: + Title: "Group additions per hour" + DataSets: + - Query: "summarize Count = count() by bin(TimeGenerated, 1h) | extend Legend = 'Total'" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "Legend" + Type: LineChart + + AdditionalQuery: + Text: "See all group additions related to user" + Query: "project TimeGenerated, EventID, Activity, Computer, MemberName, MemberAdded, MemberSid, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, SubjectAccount, SubjectUserName, SubjectUserSid, WellKnownGroupSID, WellKnownLocalSID | order by TimeGenerated desc" + +Activities: + EnabledByDefault: true + Items: + - Id: febba410-e7d6-4c63-8fe5-2b93f448b7a1 + Title: "The user has added an account to a privileged group" + Content: "The user has added an account to the privileged group, {{TargetDomainName}}{{TargetUserName}}, {{Count}} time(s)" + Description: "This activity displays the user that added an account and the account that was added to a privileged group" + QueryDefinitions: + Filter: "where ((MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true) and (TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID) | where TargetSid !in ('S-1-5-32-555')" + SummarizeBy: "SubjectAccount, TargetUserName, TargetSid" + - Id: 5ae2baf4-de7b-40f0-a861-8852266bfcd0 + Title: "The user has added an account to the Remote Desktop Users group" + Content: "The user has added an account to the Remote Desktop Users group {{Count}} time(s)" + Description: "This activity displays the account was added to Remote Desktop group" + QueryDefinitions: + Filter: "where ((MemberNameMatch == true and MemberNTDomainMatch == true) or MemberSidMatch == true) and TargetSid in ('S-1-5-32-555')" + SummarizeBy: "SubjectAccount, TargetUserName, TargetSid" + - Id: bf56473d-b9bd-4eb1-96d0-8569ec7a9003 + Title: "The user has added an account to a security group" + Content: "The user has added {{SubjectAccount}} to the {{TargetDomainName}}\\{{TargetUserName}} group" + Description: "This activity displays the user that added an account and the account that was added to a security group" + QueryDefinitions: + Filter: "where (SubjectNameMatch == true and SubjectNTDomainMatch == true) or SubjectSidMatch == true" + SummarizeBy: "SubjectAccount, TargetUserName, TargetSid" diff --git a/Insights/Account/OfficeAnomaly.yaml b/Insights/Account/OfficeAnomaly.yaml new file mode 100644 index 0000000000..08723a9ca3 --- /dev/null +++ b/Insights/Account/OfficeAnomaly.yaml @@ -0,0 +1,95 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: OfficeActivity +Provider: Sentinel +Type: KQL +BaseQuery: | + let AScoreThresh = 3; + let maxAnomalies = 3; + let BeforeRange = 14d; + let EndTime = todatetime('{{End_Time_UTC}}'); + let StartTime = todatetime('{{Start_Time_UTC}}'); + let numDays = tolong((EndTime-StartTime)/1d); + let userData = (v_Account_Name:string, v_Account_UPNSuffix:string) { + OfficeActivity + | extend splitUserId=split(UserId, '@') + | extend Account_Name = tostring(splitUserId[0]), Account_UPNSuffix = tostring(splitUserId[1]) + | where Account_Name =~ v_Account_Name and Account_UPNSuffix =~ v_Account_UPNSuffix }; + userData('{{Account_Name}}', '{{Account_UPNSuffix}}') +RequiredInputFieldsSets: + - - Account_Name + - Account_UPNSuffix +Insights: + Id: 0a5d7b14-b485-450a-a0ac-4100c860ac32 + DisplayName: Anomalously high office operation count + Description: Highlight office operations of the user with anomalously high count compared to those observed in the preceding 14 days. + DefaultTimeRange: + BeforeRange: 1d + AfterRange: 0d + ReferenceTimeRange: + BeforeRange: 14d + TableQuery: + ColumnsDefinitions: + - Header: Operation + OutputType: String + SupportDeepLink: true + - Header: Expected Count + OutputType: Number + SupportDeepLink: false + - Header: Actual Count + OutputType: Number + SupportDeepLink: false + QueriesDefinitions: + - Filter: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Operation + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost=maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore-maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + Summarize: take 1 + Project: project Operation, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) + LinkColumnsDefinitions: + - ProjectedName: Operation + Query: | + {{BaseQuery}} + | where TimeGenerated between (StartTime .. EndTime) + | where Operation == '{{RowValue_Operation}}' + ChartQuery: + Title: Anomalous operation timeline + DataSets: + - Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Operation + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost=maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore-maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,round(postExpectedCount,2)) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + | take 1 + | project Operation, TimeGenerated, count_ + | mvexpand TimeGenerated, count_ | project todatetime(TimeGenerated), toint(count_), Operation + XColumnName: TimeGenerated + YColumnName: count_ + LegendColumnName: Operation + Type: LineChart + AdditionalQuery: + Text: Query all anomalous operations + Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Operation + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh | order by maxAnomalyScorePost desc + | project Operation, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) diff --git a/Insights/Account/Office_ActedonForeignMailbox.yaml b/Insights/Account/Office_ActedonForeignMailbox.yaml new file mode 100644 index 0000000000..037c422d84 --- /dev/null +++ b/Insights/Account/Office_ActedonForeignMailbox.yaml @@ -0,0 +1,31 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: OfficeActivity +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +- - Account_Sid +BaseQuery: |- + let TLQ_UserActedOnForeignMailbox = (Account_Name:string, Account_UPNSuffix:string, account_sid:string){ + let account_upn = iff(Account_Name!="" and Account_UPNSuffix != "" + ,strcat(Account_Name,"@",Account_UPNSuffix) + ,""); + OfficeActivity + | where RecordType == "ExchangeItem" and UserType =="Regular" and Operation !contains "InboxRule" + | where LogonUserSid != MailboxOwnerSid + | where ((account_sid != "" and LogonUserSid =~ account_sid) + or ( account_upn != "" and UserId =~ account_upn )) + }; + TLQ_UserActedOnForeignMailbox('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_Sid}}') + +Activities: + EnabledByDefault: true + Title: "The user acted on another accounts mailbox" + Content: "The user acted on mailbox {{MailboxOwnerUPN}} {{Count}} time(s)" + Items: + - Id: 1f82f263-d694-469a-9717-1b3edf9d3bb2 + Description: This activity lists user's activities on others' mailbox + QueryDefinitions: + SummarizeBy: MailboxOwnerSid, MailboxOwnerUPN diff --git a/Insights/Account/Office_ChangeInboxRules.yaml b/Insights/Account/Office_ChangeInboxRules.yaml new file mode 100644 index 0000000000..bf44c9c03f --- /dev/null +++ b/Insights/Account/Office_ChangeInboxRules.yaml @@ -0,0 +1,32 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: OfficeActivity +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +- - Account_Sid +BaseQuery: |- + let ruleChangeRecordTypes = dynamic( ["ExchangeAdmin", "ExchangeItem"]); + let TLQ_UserModifiedinboxRules = (Account_Name: string, Account_UPNSuffix: string, Account_Sid: string){ + let upn = iff(Account_Name != "" and Account_UPNSuffix != "" + , strcat(Account_Name, "@", Account_UPNSuffix) + , ""); + OfficeActivity + | where RecordType in~ (ruleChangeRecordTypes) and Operation contains "InboxRule" + | where((Account_Sid != "" and LogonUserSid == Account_Sid) + or(upn != "" and UserId == upn ) + ) + }; + TLQ_UserModifiedinboxRules('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_Sid}}') + +Activities: + EnabledByDefault: true + Title: "The user modified inbox rules on another accounts mailbox" + Content: "User Modified {{Count}} inbox rules on {{MailboxOwnerUPN}}'s Mailbox" + Items: + - Id: e480efd0-016d-428e-b892-84b9d586d004 + Description: User modified inbox rules on a mailbox + QueryDefinitions: + SummarizeBy: MailboxOwnerSid, MailboxOwnerUPN diff --git a/Insights/Account/Office_SharePointUploads.yaml b/Insights/Account/Office_SharePointUploads.yaml new file mode 100644 index 0000000000..1cb27a4537 --- /dev/null +++ b/Insights/Account/Office_SharePointUploads.yaml @@ -0,0 +1,34 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: OfficeActivity +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +BaseQuery: |- + let TLQ_UserUploadFiles = (Account_Name:string, Account_UPNSuffix:string){ + let upn = strcat(Account_Name,"@",Account_UPNSuffix); + OfficeActivity + | where RecordType =~ "SharePointFileOperation" and Operation in~ ("FileUploaded", "FileDownloaded") + | where upn =~UserId + | extend Subject_File_Directory = tostring(split(OfficeObjectId,SourceFileName)[0]), Op = iff (Operation != "FileUploaded", "uploaded", "downloaded") + | project-rename Source_IP_Address = ClientIP + }; + TLQ_UserUploadFiles('{{Account_Name}}', '{{Account_UPNSuffix}}') + +Activities: + EnabledByDefault: true + Title: "User {{Op}} files to SharePoint" + Content: "User Uploaded {{Count}} File(s) To SharePoint From {{Source IPAddress}}" + Items: + - Id: 0eabec03-51e7-4909-b0cb-1adc76759e93 + Description: This activity lists the user's SharePoint uploads. + QueryDefinitions: + Filter: where Operation =~ "FileUploaded" + SummarizeBy: Source_IP_Address, Op + - Id: df564e7b-bf6d-4dc4-a32d-79b00bd2cc7b + Description: This activity lists the user's SharePoint downloads. + QueryDefinitions: + Filter: where Operation =~ "FileDownloaded" + SummarizeBy: Source_IP_Address, Op diff --git a/Insights/Account/ResourceAccess.yaml b/Insights/Account/ResourceAccess.yaml new file mode 100644 index 0000000000..1b672922ef --- /dev/null +++ b/Insights/Account/ResourceAccess.yaml @@ -0,0 +1,75 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: OfficeActivity +Provider: Sentinel +Type: KQL +RequiredInputFieldsSets: + - - Account_Name + - Account_UPNSuffix + - - Account_AADUserId +BaseQuery: | + let Operations = dynamic(["FileDownloaded", "FileUploaded"]); + let UserOperationToSharePoint = (v_Account_Name:string, v_Account_UPNSuffix:string) { + OfficeActivity + // Select sharepoint activity that is relevant + | where RecordType in~ ('SharePointFileOperation') + | where Operation in~ (Operations) + | extend Account_Name = tostring(split(UserId, '@')[0]) + | extend Account_UPNSuffix = tostring(split(UserId, '@')[1]) + | where Account_Name =~ v_Account_Name and Account_UPNSuffix =~ v_Account_UPNSuffix + | project TimeGenerated, Account_Name, Account_UPNSuffix, UserId, OfficeId, RecordType, Operation, OrganizationId, UserType, UserKey, OfficeWorkload, OfficeObjectId, ClientIP, ItemType, UserAgent, Site_Url, SourceRelativeUrl, SourceFileName, SourceFileExtension , Start_Time , ElevationTime , TenantId, SourceSystem , Type + }; + UserOperationToSharePoint ('{{Account_Name}}','{{Account_UPNSuffix}}') +Insights: + Id: e6cf68e6-1eca-4fbb-9fad-6280f2a9476e + DisplayName: Resource access + Description: | + Provides the count and distinct resource accesses by a given user account + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: Resource Type + OutputType: String + - Header: Distinct Resources + OutputType: Number + SupportDeepLink: true + - Header: Total Resources + OutputType: Number + SupportDeepLink: true + - Header: IPAddress(es) + OutputType: string + QueriesDefinitions: + - Filter: "where Operation =~ 'FileUploaded'" + Summarize: "summarize DistinctResources = dcount(SourceFileName), TotalResources = count(SourceFileName), IPAddresses = make_set(ClientIP) by Operation" + Project: "project Title = Operation, DistinctResources, TotalResources, IPAddresses = case(array_length(IPAddresses) == 1, tostring(IPAddresses[0]), array_length(IPAddresses) > 1, 'Many', 'None')" + LinkColumnsDefinitions: + - ProjectedName: DistinctResources + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: TotalResources + Query: "{{BaseQuery}} | {{RowFilter}}" + - Filter: "where Operation =~ 'FileDownloaded'" + Summarize: "summarize DistinctResources = dcount(SourceFileName), TotalResources = count(SourceFileName), IPAddresses = make_set(ClientIP) by Operation" + Project: "project Title = Operation, DistinctResources, TotalResources, IPAddresses = case(array_length(IPAddresses) == 1, tostring(IPAddresses[0]), array_length(IPAddresses) > 1, 'Many', 'None')" + LinkColumnsDefinitions: + - ProjectedName: DistinctResources + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: TotalResources + Query: "{{BaseQuery}} | {{RowFilter}}" + ChartQuery: + Title: "Resource access over time" + DataSets: + - Query: "summarize DistinctResources = dcountif(Operation, Operation =~ 'FileUploaded'), TotalResources = countif(Operation =~ 'FileUploaded') by bin(TimeGenerated, 1h) | extend Legend = 'File Uploads'" + XColumnName: TimeGenerated + YColumnName: TotalResources + LegendColumnName: Legend + - Query: "summarize DistinctResources = dcountif(Operation, Operation =~ 'FileDownloaded'), TotalResources = countif(Operation =~ 'FileDownloaded') by bin(TimeGenerated, 1h) | extend Legend = 'File Downloads'" + XColumnName: TimeGenerated + YColumnName: TotalResources + LegendColumnName: Legend + Type: LineChart + AdditionalQuery: + Text: "See all resource activity" + Query: "where Operation in~ (Operations)" + \ No newline at end of file diff --git a/Insights/Account/SignInAnomaly.yaml b/Insights/Account/SignInAnomaly.yaml new file mode 100644 index 0000000000..0ff93449ea --- /dev/null +++ b/Insights/Account/SignInAnomaly.yaml @@ -0,0 +1,99 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SigninLogs +Type: KQL +Provider: Sentinel +BaseQuery: | + let AScoreThresh=3; + let maxAnomalies=3; + let BeforeRange = 14d; + let EndTime=todatetime('{{End_Time_UTC}}'); + let StartTime = todatetime('{{Start_Time_UTC}}'); + let numDays = tolong((EndTime-StartTime)/1d); + let userData = (v_Account_Name:string, v_Account_UPNSuffix:string, v_Account_AADUserId:string) { + SigninLogs + | where TimeGenerated between ((StartTime-BeforeRange) .. EndTime) + | extend splitUserId=split(UserPrincipalName, '@') + | extend Account_Name = tostring(splitUserId[0]), Account_UPNSuffix = tostring(splitUserId[1]) + | where (Account_Name =~ v_Account_Name and Account_UPNSuffix =~ v_Account_UPNSuffix) or UserId =~ v_Account_AADUserId }; + userData('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_AADUserId}}') +RequiredInputFieldsSets: + - - Account_Name + - Account_UPNSuffix + - - Account_AADUserId +Insights: + Id: cae8d0aa-aa45-4d53-8d88-17dd64ffd4e4 + DisplayName: Anomalously high Azure sign-in result count + Description: Highlight Azure sign-in results by the user principal with anomalously high count compared to those observed in the preceding 14 days. + DefaultTimeRange: + BeforeRange: 1d + AfterRange: 0d + ReferenceTimeRange: + BeforeRange: 14d + TableQuery: + ColumnsDefinitions: + - Header: Result Description + OutputType: String + SupportDeepLink: true + - Header: Expected Count + OutputType: Number + SupportDeepLink: false + - Header: Actual Count + OutputType: Number + SupportDeepLink: false + QueriesDefinitions: + - Filter: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by ResultDescription + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + Summarize: take 1 + Project: project ResultDescription, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) + LinkColumnsDefinitions: + - ProjectedName: ResultDescription + Query: | + {{BaseQuery}} + | where TimeGenerated between (StartTime .. EndTime) + | where ResultDescription == '{{RowValue_ResultDescription}}' + ChartQuery: + Title: Anomalous sign-in result timeline + DataSets: + - Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by ResultDescription + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,round(postExpectedCount,2)) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + | take 1 + | project ResultDescription, TimeGenerated, count_ + | mvexpand TimeGenerated, count_ + | project todatetime(TimeGenerated), toint(count_), ResultDescription + XColumnName: TimeGenerated + YColumnName: count_ + LegendColumnName: ResultDescription + Type: LineChart + AdditionalQuery: + Text: Query all anomalous sign-in results + Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by ResultDescription + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + | project ResultDescription, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) diff --git a/Insights/Account/Signins_byResources.yaml b/Insights/Account/Signins_byResources.yaml new file mode 100644 index 0000000000..e2bfa6d9cb --- /dev/null +++ b/Insights/Account/Signins_byResources.yaml @@ -0,0 +1,28 @@ +SchemaVersion: 1.0 +Provider: Sentinel +Type: KQL +DataTypes: +- DataType: SigninLogs +RequiredInputFieldsSets: +- - Account_Name + - Account_UPNSuffix +- - Account_AadUserId +BaseQuery: |- + let SignInsByResource = (Account_Name:string, Account_UPNSuffix:string, Account_AadUserId:string){ + let acc_upn = iff(Account_Name != "" and Account_UPNSuffix != "" ,strcat(Account_Name,"@" ,Account_UPNSuffix),""); + SigninLogs + | where (acc_upn != "" and UserPrincipalName =~ acc_upn) or +    (Account_AadUserId != "" and Account_AadUserId =~ UserId) // UserPrincipalName, UserId + | extend shortResourceId = tostring(split(ResourceId,"/")[-1]) + }; + SignInsByResource('{{Account_Name}}', '{{Account_UPNSuffix}}', '{{Account_AadUserId}}') + +Activities: + EnabledByDefault: true + Title: "The user signed in to an Azure resource" + Content: "The user signed in to {{shortResourceId}} {{Count}} time(s)" + Items: + - Id: 1f82f263-d694-469a-9717-1b3edf9d3bb2 + Description: This activity lists user's sign ins to Azure Resources + QueryDefinitions: + SummarizeBy: shortResourceId, ResourceId diff --git a/Insights/Account/WindowsSigninActivity.yaml b/Insights/Account/WindowsSigninActivity.yaml new file mode 100644 index 0000000000..1bcf9b2188 --- /dev/null +++ b/Insights/Account/WindowsSigninActivity.yaml @@ -0,0 +1,272 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +RequiredInputFieldsSets: + - - Account_Name + - Account_NTDomain +BaseQuery: | + let GetAllLogonsForUser = (v_Account_Name:string, v_Account_NTDomain:string){ + let AllEvents = SecurityEvent + | extend p_Account_Name = case( + // Handles mixed use scenario of NTDomain\AccountName@UPNSuffix + v_Account_Name has '@' and v_Account_Name has '\\', tostring(split(tostring(split(v_Account_Name, '\\')[1]),'@')[0]), + v_Account_Name has '@', tostring(split(v_Account_Name, '@')[0]), + v_Account_Name has '\\', tostring(split(v_Account_Name, '\\')[1]), + v_Account_Name + ) + | extend p_Account_NTDomain = case( + v_Account_NTDomain has '\\', tostring(split(v_Account_NTDomain, '\\')[0]), + // Handles UPN scenario of AccountName@UPNSuffix to pull potential NTDomain from + v_Account_NTDomain has '@', tostring(split(tostring(split(v_Account_NTDomain, '@')[1]),'.')[0]), + v_Account_NTDomain + ) + | where EventID in (4624, 4625, 4672) + | where AccountType =~ 'User' + | where TargetUserName =~ p_Account_Name and TargetDomainName =~ p_Account_NTDomain + | extend PassedInAccountName = p_Account_Name, PassedInNTDomain = p_Account_NTDomain, RelatedRowSet = 'AllEvents' + | extend HourOfLogin = hourofday(TimeGenerated), DayNumberofWeek = dayofweek(TimeGenerated) + | extend DayofWeek = case( + DayNumberofWeek == "00:00:00", "Sunday", + DayNumberofWeek == "1.00:00:00", "Monday", + DayNumberofWeek == "2.00:00:00", "Tuesday", + DayNumberofWeek == "3.00:00:00", "Wednesday", + DayNumberofWeek == "4.00:00:00", "Thursday", + DayNumberofWeek == "5.00:00:00", "Friday", + DayNumberofWeek == "6.00:00:00", "Saturday","InvalidTimeStamp") + // map the most common ntstatus codes + | extend StatusDesc = case( + Status =~ "0x80090302", "SEC_E_UNSUPPORTED_FUNCTION", + Status =~ "0x80090308", "SEC_E_INVALID_TOKEN", + Status =~ "0x8009030E", "SEC_E_NO_CREDENTIALS", + Status =~ "0xC0000008", "STATUS_INVALID_HANDLE", + Status =~ "0xC0000017", "STATUS_NO_MEMORY", + Status =~ "0xC0000022", "STATUS_ACCESS_DENIED", + Status =~ "0xC0000034", "STATUS_OBJECT_NAME_NOT_FOUND", + Status =~ "0xC000005E", "STATUS_NO_LOGON_SERVERS", + Status =~ "0xC000006A", "STATUS_WRONG_PASSWORD", + Status =~ "0xC000006D", "STATUS_LOGON_FAILURE", + Status =~ "0xC000006E", "STATUS_ACCOUNT_RESTRICTION", + Status =~ "0xC0000073", "STATUS_NONE_MAPPED", + Status =~ "0xC00000FE", "STATUS_NO_SUCH_PACKAGE", + Status =~ "0xC000009A", "STATUS_INSUFFICIENT_RESOURCES", + Status =~ "0xC00000DC", "STATUS_INVALID_SERVER_STATE", + Status =~ "0xC0000106", "STATUS_NAME_TOO_LONG", + Status =~ "0xC000010B", "STATUS_INVALID_LOGON_TYPE", + Status =~ "0xC000015B", "STATUS_LOGON_TYPE_NOT_GRANTED", + Status =~ "0xC000018B", "STATUS_NO_TRUST_SAM_ACCOUNT", + Status =~ "0xC0000224", "STATUS_PASSWORD_MUST_CHANGE", + Status =~ "0xC0000234", "STATUS_ACCOUNT_LOCKED_OUT", + Status =~ "0xC00002EE", "STATUS_UNFINISHED_CONTEXT_DELETED", + EventID == 4624 or EventID == 4672, "Success", + "See - https://docs.microsoft.com/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55" + ) + | extend SubStatusDesc = case( + SubStatus =~ "0x80090325", "SEC_E_UNTRUSTED_ROOT", + SubStatus =~ "0xC0000008", "STATUS_INVALID_HANDLE", + SubStatus =~ "0xC0000022", "STATUS_ACCESS_DENIED", + SubStatus =~ "0xC0000064", "STATUS_NO_SUCH_USER", + SubStatus =~ "0xC000006A", "STATUS_WRONG_PASSWORD", + SubStatus =~ "0xC000006D", "STATUS_LOGON_FAILURE", + SubStatus =~ "0xC000006E", "STATUS_ACCOUNT_RESTRICTION", + SubStatus =~ "0xC000006F", "STATUS_INVALID_LOGON_HOURS", + SubStatus =~ "0xC0000070", "STATUS_INVALID_WORKSTATION", + SubStatus =~ "0xC0000071", "STATUS_PASSWORD_EXPIRED", + SubStatus =~ "0xC0000072", "STATUS_ACCOUNT_DISABLED", + SubStatus =~ "0xC0000073", "STATUS_NONE_MAPPED", + SubStatus =~ "0xC00000DC", "STATUS_INVALID_SERVER_STATE", + SubStatus =~ "0xC0000133", "STATUS_TIME_DIFFERENCE_AT_DC", + SubStatus =~ "0xC000018D", "STATUS_TRUSTED_RELATIONSHIP_FAILURE", + SubStatus =~ "0xC0000193", "STATUS_ACCOUNT_EXPIRED", + SubStatus =~ "0xC0000380", "STATUS_SMARTCARD_WRONG_PIN", + SubStatus =~ "0xC0000381", "STATUS_SMARTCARD_CARD_BLOCKED", + SubStatus =~ "0xC0000382", "STATUS_SMARTCARD_CARD_NOT_AUTHENTICATED", + SubStatus =~ "0xC0000383", "STATUS_SMARTCARD_NO_CARD", + SubStatus =~ "0xC0000384", "STATUS_SMARTCARD_NO_KEY_CONTAINER", + SubStatus =~ "0xC0000385", "STATUS_SMARTCARD_NO_CERTIFICATE", + SubStatus =~ "0xC0000386", "STATUS_SMARTCARD_NO_KEYSET", + SubStatus =~ "0xC0000387", "STATUS_SMARTCARD_IO_ERROR", + SubStatus =~ "0xC0000388", "STATUS_DOWNGRADE_DETECTED", + SubStatus =~ "0xC0000389", "STATUS_SMARTCARD_CERT_REVOKED", + EventID == 4624 or EventID == 4672, "Success", + "See - https://docs.microsoft.com/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55" + ) + | project StartTime = TimeGenerated, DayofWeek, HourOfLogin, EventID, Activity, IpAddress, WorkstationName, Computer, TargetUserName, TargetDomainName, ProcessName, SubjectUserName, PrivilegeList, PassedInAccountName, PassedInNTDomain, LogonTypeName, StatusDesc, SubStatusDesc, RelatedRowSet + ; + let UserSigninToSystems = AllEvents + | where EventID == 4624 + | project-away StatusDesc, SubStatusDesc, PrivilegeList + | summarize Total= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=max(StartTime), EndTime = min(StartTime), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), HostLoggedOn = make_set(Computer) by EventID, Activity, TargetDomainName, TargetUserName , ProcessName , LogonTypeName + | extend RelatedRowSet = 'UserSigninToSystems' ; + let UserFailedSigninToSystems = AllEvents + | where EventID == 4625 + | project-away PrivilegeList + | summarize Total= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=max(StartTime), EndTime = min(StartTime), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), HostLoggedOn = make_set(Computer) by EventID, Activity, TargetDomainName, TargetUserName , ProcessName , LogonTypeName + | extend RelatedRowSet = 'UserFailedSigninToSystems' ; + let UserSigninDuringAbnormalHours = AllEvents + | where StartTime between (ago(14d)..ago(2d)) + | where EventID in (4624,4625) + | where LogonTypeName in~ ('2 - Interactive','10 - RemoteInteractive') + | summarize max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek) by TargetUserName + | join kind= inner + ( + AllEvents + | where StartTime > ago(2d) + | where LogonTypeName in~ ('2 - Interactive','10 - RemoteInteractive') + ) + on TargetUserName + | where HourOfLogin > max_HourOfLogin or HourOfLogin < min_HourOfLogin + | extend historical_DayofWeek = tostring(historical_DayofWeek) + | summarize Total= count(), max(HourOfLogin), min(HourOfLogin), current_DayofWeek =make_set(DayofWeek), StartTime=max(StartTime), EndTime = min(StartTime), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), HostLoggedOn = make_set(Computer) by EventID, Activity, TargetDomainName, TargetUserName , ProcessName , LogonTypeName, StatusDesc, SubStatusDesc, historical_DayofWeek + | extend historical_DayofWeek = todynamic(historical_DayofWeek) + | extend RelatedRowSet = 'UserSigninDuringAbnormalHour'; + let UserHadPrivilegedLogonSessions = AllEvents + | where EventID == 4672 + | where PrivilegeList contains 'SeDebugPrivilege' + | project-away StatusDesc, SubStatusDesc + | summarize Total= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=max(StartTime), EndTime = min(StartTime), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), HostLoggedOn = make_set(Computer) by EventID, Activity, PrivilegeList + // Notice! summarize removes the TimeGenerated field, which is required for Activities. + | extend RelatedRowSet = 'UserHadPrivilegedLogonSessions' ; + union isfuzzy=true AllEvents, UserSigninToSystems, UserFailedSigninToSystems, UserSigninDuringAbnormalHours, UserHadPrivilegedLogonSessions + }; + // change {{Account_Name}} value below to the username you are interested in and {{Account_NTDomain}} to the domain of the user you are interested in + GetAllLogonsForUser('{{Account_Name}}', '{{Account_NTDomain}}') +Insights: + Id: 8d209299-cb14-4f22-b5c5-6813f2d1ed2e + DisplayName: Windows sign-in activity + Description: | + Summary of successful and failed sign-ins along with anamalous sign-in patterns for the specific user. Successful sign-ins currently only include interactive and limited to LogonType 2 and 10. + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: "Signin Type" + OutputType: String + - Header: TotalLogons + OutputType: Number + SupportDeepLink: true + - Header: HostCount + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + + # UserSigninToSystems + - Filter: "where RelatedRowSet =~ 'UserSigninToSystems' | extend NumberOfHostsLoggedOn = array_length(HostLoggedOn) " + Summarize: "summarize TotalLogons = sum(Total), HostCount = sum(NumberOfHostsLoggedOn)" + Project: "project Title = 'Successful Signins', TotalLogons, HostCount" + LinkColumnsDefinitions: + - ProjectedName: TotalLogons + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserFailedSigninToSystems + - Filter: "where RelatedRowSet =~ 'UserFailedSigninToSystems'| extend NumberOfHostsLoggedOn = array_length(HostLoggedOn) " + Summarize: "summarize TotalLogons = sum(Total), HostCount = sum(NumberOfHostsLoggedOn)" + Project: "project Title = 'Failed Signins', TotalLogons, HostCount" + LinkColumnsDefinitions: + - ProjectedName: TotalLogons + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserSigninDuringAbnormalHours + - Filter: "where RelatedRowSet =~ 'UserSigninDuringAbnormalHour' | extend NumberOfHostsLoggedOn = array_length(HostLoggedOn)" + Summarize: "summarize TotalLogons = sum(Total), HostCount = sum(NumberOfHostsLoggedOn)" + Project: "project Title = 'Abnormal Time Signins', TotalLogons, HostCount" + LinkColumnsDefinitions: + - ProjectedName: TotalLogons + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserHadPrivilegedLogonSessions + - Filter: "where RelatedRowSet =~ 'UserHadPrivilegedLogonSessions' | extend NumberOfHostsLoggedOn = array_length(HostLoggedOn) " + Summarize: "summarize TotalLogons = sum(Total), HostCount = sum(NumberOfHostsLoggedOn)" + Project: "project Title = 'Privileged Signins', TotalLogons, HostCount" + LinkColumnsDefinitions: + - ProjectedName: TotalLogons + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: HostCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + + ChartQuery: + Title: "Sign-ins over time" + DataSets: + - Query: "summarize Count=countif(EventID==4624) by Time = bin(StartTime, 1h) | extend Legend = 'Success'" + XColumnName: "Time" + YColumnName: "Count" + LegendColumnName: "Legend" + - Query: "summarize Count=countif(EventID==4625) by Time = bin(StartTime, 1h) | extend Legend = 'Failed'" + XColumnName: "Time" + YColumnName: "Count" + LegendColumnName: "Legend" + Type: LineChart + + AdditionalQuery: + Text: "See all Windows sign-ins" + Query: "where RelatedRowSet =~ 'AllEvents' | where EventID in (4624,4625,4672) | extend SubjectUserName = columnifexists('SubjectUserName', 'EventDoesNotContain') | summarize Total= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=max(StartTime), EndTime = min(StartTime), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), HostLoggedOn = make_set(Computer) by Activity, TargetDomainName, TargetUserName, ProcessName, LogonTypeName | extend NumberOfHostsLoggedOn = array_length(HostLoggedOn)" +Activities: + EnabledByDefault: true + Title: "{{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Items: + - Id: 0d4ec12e-e44a-40a4-bb87-3db84d2a8057 + Title: "{{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's interactive log-ins grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet == 'UserSigninToSystems' and LogonTypeName == '2 - Interactive' | extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: c9da5786-6c3c-45b5-9a46-53200ed9df09 + Title: "{{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's network log-ins, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet == 'UserSigninToSystems' and LogonTypeName == '3 - Network' | extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: 8a302bfc-00e3-43b3-a516-102fd0cb0dbc + Title: "{{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's remote interactive log-ins, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet == 'UserSigninToSystems' and LogonTypeName == '10 - RemoteInteractive'| extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: ec87b066-17ad-4f9b-97c2-c2f2ee2d99e0 + Title: "{{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's log-ins with new credentials, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet == 'UserSigninToSystems' and LogonTypeName == '9 - NewCredentials'| extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: e1c4c03c-2b40-47cf-9b8c-49e0a37a6da6 + Title: "'Privileged log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's privileged log-ins, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet = 'UserHadPrivilegedLogonSessions'" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: a6fc3ad9-1a61-41f5-a5e2-bd1f5a6fe44d + Title: "'Failed {{LogonTypeName}}' log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's failed interactive log-ins grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet =~ 'UserFailedSigninToSystems' and LogonTypeName == '2 - Interactive' | extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: 11449689-6542-4867-86dc-56264abbd90c + Title: "'Failed {{LogonTypeName}}' log-ins to a host" + Content: "The user {{v_Account_Name}} logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's failed network log-ins, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet =~ 'UserFailedSigninToSystems' and LogonTypeName == '3 - Network' | extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" + - Id: 686cf7e8-87c7-4391-8898-25adf1033a54 + Title: "Failed {{LogonTypeName}} log-ins to a host" + Content: "The user {{v_Account_Name}} failed to logged on to host {{WorkstationName}} {{Count}} time(s)" + Description: "This activity lists the user's failed remote interactive log-ins, grouped by Host." + QueryDefinitions: + Filter: "where RelatedRowSet =~ 'UserFailedSigninToSystems' and LogonTypeName == '10 - RemoteInteractive' | extend TimeGenerated=StartTime" + SummarizeBy: "Computer, WorkstationName, LogonTypeName" diff --git a/Insights/Host/ActionsOnAccounts.yaml b/Insights/Host/ActionsOnAccounts.yaml new file mode 100644 index 0000000000..4f9c968d21 --- /dev/null +++ b/Insights/Host/ActionsOnAccounts.yaml @@ -0,0 +1,136 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + let GetAccountActions = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string){ + SecurityEvent + | where EventID in (4725, 4726, 4767, 4720, 4722, 4723, 4724) + // parsing for Host to handle variety of conventions coming from data + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | project TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, SubjectUserName, SubjectUserSid, _ResourceId, SourceComputerId + | extend AddedBy = SubjectUserName + // Future support for Activities + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, AccountCustomEntity = TargetAccount + }; + GetAccountActions('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +# The queries for the insights. +Insights: + Id: e29ee1ef-7445-455e-85f1-269f2d536d61 + DisplayName: Actions on accounts + Description: | + Summary of actions taken on the specified host, grouped by action: password resets and changes, account lockouts (policy or admin), account creation and deletion, account enabled and disabled + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: "Action" + OutputType: String + SupportDeepLink: false + - Header: "NameCount" + OutputType: Number + SupportDeepLink: true + - Header: "SIDCount" + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + # Account Created or Enabled + - Filter: "where EventID in (4720, 4722)" + Summarize: "summarize NameCount = dcount(TargetAccount), SIDCount = dcount(TargetSid)" + Project: "project Title = 'Created or Enabled', NameCount, SIDCount" + LinkColumnsDefinitions: + - ProjectedName: NameCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: SIDCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # Account Deleted or Disabled + - Filter: "where EventID in (4725, 4726)" + Summarize: "summarize NameCount = dcount(TargetAccount), SIDCount = dcount(TargetSid)" + Project: "project Title = 'Deleted or Disabled', NameCount, SIDCount" + LinkColumnsDefinitions: + - ProjectedName: NameCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: SIDCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # Account Password Changes + - Filter: "where EventID in (4723, 4724)" + Summarize: "summarize NameCount = dcount(TargetAccount), SIDCount = dcount(TargetSid)" + Project: "project Title = 'Password Reset', NameCount, SIDCount" + LinkColumnsDefinitions: + - ProjectedName: NameCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: SIDCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + + ChartQuery: + Title: "Actions on accounts over time" + DataSets: + - Query: "where EventID in (4720, 4722) | summarize Count = dcount(TargetSid) by bin(TimeGenerated, 1h) | extend Legend = 'Created/Enabled'" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "Legend" + - Query: "where EventID in (4725, 4726) | summarize Count = dcount(TargetSid) by bin(TimeGenerated, 1h) | extend Legend = 'Deleted/Disabled'" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "Legend" + - Query: "where EventID in (4723, 4724) | summarize Count = dcount(TargetSid) by bin(TimeGenerated, 1h) | extend Legend = 'Reset'" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "Legend" + Type: BarChart + + AdditionalQuery: + Text: "See all actions on accounts" + Query: "project TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, SubjectUserName, SubjectUserSid, _ResourceId, SourceComputerId, timestamp, HostCustomEntity, AccountCustomEntity | order by TimeGenerated desc" + +Activities: + EnabledByDefault: true + Items: + - Id: 307c85ee-39a2-4da3-952e-4fd79aa46d3a + Description: Account created on host + Title: "An account was created on this host" + Content: "On '{{Computer}}' the account '{{TargetAccount}}' was created by '{{AddedBy}}'" + QueryDefinitions: + Filter: where EventID == 4720 + SummarizeBy: Computer + - Id: 31529548-dbd2-4d5d-8270-710330cdcec7 + Description: Account deleted on host + Title: "An account was deleted on this host" + Content: "On '{{Computer}}' the account '{{TargetAccount}}' was deleted by '{{AddedBy}}'" + QueryDefinitions: + Filter: where EventID == 4726 + SummarizeBy: Computer \ No newline at end of file diff --git a/Insights/Host/EventLogClear.yaml b/Insights/Host/EventLogClear.yaml new file mode 100644 index 0000000000..8e6cc94c96 --- /dev/null +++ b/Insights/Host/EventLogClear.yaml @@ -0,0 +1,180 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: SecurityEvent + - DataType: Event +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + let SystemAccount = datatable(AccountName:string)['NT AUTHORITY\\SYSTEM', 'NT AUTHORITY\\NETWORK SERVICE', 'NT AUTHORITY\\LOCAL SERVICE', 'NT AUTHORITY\\IUSR', 'NTAUTHORITY\\ANONYMOUS LOGON']; + let SvcAcctList = dynamic(["Local SYSTEM","Local SERVICE","Network SERVICE","NT AUTHORITY"]); + let ServiceAccount = SecurityEvent + | where EventID == '4624' and LogonType == '5' and not(Account has_any (SvcAcctList)) + | extend AccountName = Account + | distinct AccountName; + let MachineAccount = SecurityEvent + | where EventID == '4624' and AccountType == "Machine" and not(Account has_any (SvcAcctList)) + | extend AccountName = Account + | distinct AccountName; + let Accounts = union isfuzzy=true SystemAccount, ServiceAccount, MachineAccount; + let source = 'Microsoft-Windows-Eventlog'; + let tableFunc = (tableName:string, event:int){ + table(tableName) + | where EventID == event + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | extend SourceComputerId = column_ifexists("SourceComputerId", "NotAvailable"), EventOriginId = column_ifexists("EventOriginId", "NotAvailable") + | parse EventData with * 'SubjectUserName>' SubjectUserName '<' * + | parse EventData with * 'SubjectUserSid>' SubjectUserSid '<' * + | parse EventData with * 'SubjectLogonId>' SubjectLogonId '<' * + | parse EventData with * 'SubjectDomainName>' SubjectDomainName '<' * + | extend SubjectAccount = strcat(SubjectDomainName, '\\', SubjectUserName) + }; + let HostClearedEventLog = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string) + { + let Event104 = tableFunc('Event', event=104) + | where Source =~ source + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | parse RenderedDescription with * 'The' LogName 'log' * + | project TimeGenerated, Computer, EventID, SubjectAccount, SubjectUserName, SubjectDomainName, LogName, SubjectUserSid, SubjectLogonId, SourceComputerId, EventOriginId, _ResourceId + | extend timestamp = TimeGenerated, AccountCustomEntity = SubjectAccount, HostCustomEntity = Computer; + let Event1102 = tableFunc('SecurityEvent', event=1102) + | where EventSourceName == source + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | extend LogName = 'Security' + | project TimeGenerated, Computer, EventID, SubjectAccount, SubjectUserName, SubjectDomainName, LogName, SubjectUserSid, SubjectLogonId, SourceComputerId, EventOriginId, _ResourceId + | extend timestamp = TimeGenerated, AccountCustomEntity = SubjectAccount, HostCustomEntity = Computer; + union isfuzzy=true Event104, Event1102 + }; + HostClearedEventLog('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +Insights: + Id: 9a70a72d-25d4-7212-b73e-4f302a90c06a + DisplayName: Event Logs cleared on host + Description: | + 'Provides the number of times event logs were cleared on the host.' + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: "Cleared By" + OutputType: String + SupportDeepLink: false + - Header: "Security Log" + OutputType: Number + SupportDeepLink: true + - Header: "Other Logs" + OutputType: Number + SupportDeepLink: true + - Header: "Total" + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + + # LogCleared_User + - Filter: "where SubjectUserName !in (Accounts)" + Summarize: "summarize Security = countif(LogName =~ 'Security'), Other = countif(LogName !~ 'Security'), All = count() by SubjectAccount" + Project: "project SubjectAccount, Security, Other, All" + LinkColumnsDefinitions: + - ProjectedName: Security + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName =~ 'Security'" + - ProjectedName: Other + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName !~ 'Security'" + - ProjectedName: All + Query: "{{BaseQuery}} | {{RowFilter}}" + + # LogCleared_System + - Filter: "where AccountCustomEntity in (SystemAccount)" + Summarize: "summarize Security = countif(LogName =~ 'Security'), Other = countif(LogName !~ 'Security'), All = count() by SubjectAccount" + Project: "project SubjectAccount, Security, Other, All" + LinkColumnsDefinitions: + - ProjectedName: Security + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName =~ 'Security'" + - ProjectedName: Other + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName !~ 'Security'" + - ProjectedName: All + Query: "{{BaseQuery}} | {{RowFilter}}" + + # LogCleared_Service + - Filter: "where AccountCustomEntity in (ServiceAccount)" + Summarize: "summarize Security = countif(LogName =~ 'Security'), Other = countif(LogName !~ 'Security'), All = count() by SubjectAccount" + Project: "project SubjectAccount, Security, Other, All" + LinkColumnsDefinitions: + - ProjectedName: Security + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName =~ 'Security'" + - ProjectedName: Other + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName !~ 'Security'" + - ProjectedName: All + Query: "{{BaseQuery}} | {{RowFilter}}" + + # LogCleared_Machine + - Filter: "where AccountCustomEntity in (MachineAccount)" + Summarize: "summarize Security = countif(LogName =~ 'Security'), Other = countif(LogName !~ 'Security'), All = count() by SubjectAccount" + Project: "project SubjectAccount, Security, Other, All" + LinkColumnsDefinitions: + - ProjectedName: Security + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName =~ 'Security'" + - ProjectedName: Other + Query: "{{BaseQuery}} | {{RowFilter}} | where LogName !~ 'Security'" + - ProjectedName: All + Query: "{{BaseQuery}} | {{RowFilter}}" + + ChartQuery: + Title: "Log clear activity over time" + DataSets: + - Query: "summarize LogClearOnHost = count() by Time = bin(TimeGenerated, 12h), SubjectAccount" + XColumnName: Time + YColumnName: LogClearOnHost + LegendColumnName: SubjectAccount + Type: BarChart + AdditionalQuery: + Text: "See all log clear activity" + Query: "project TimeGenerated, LogName, Computer, SubjectAccount, SubjectUserName, SubjectDomainName, EventID, SubjectUserSid, SubjectLogonId, SourceComputerId, _ResourceId, EventOriginId" + +Activities: + EnabledByDefault: true + Items: + - Id: 2fcda698-9526-454f-8fe0-4a0fd7af13f2 + Description: Security Event log cleared by account + Title: "Security Event log cleared by account on this host" + Content: "On '{{Computer}}' the user '{{SubjectAccount}}' cleared the '{{LogName}}' log, EventID: '{{EventID}}'" + QueryDefinitions: + Filter: where LogName =~ 'Security' + SummarizeBy: SubjectAccount + - Id: 3ff675ee-3052-4e0b-88ad-f34ed1732adc + Description: Event logs cleared by account + Title: "Event log(s) cleared by account on this host" + Content: "On '{{Computer}}' the user '{{SubjectAccount}}' cleared the '{{LogName}}' log, EventID: '{{EventID}}'" + QueryDefinitions: + Filter: where LogName !~ 'Security' + SummarizeBy: SubjectAccount \ No newline at end of file diff --git a/Insights/Host/GroupAdditions.yaml b/Insights/Host/GroupAdditions.yaml new file mode 100644 index 0000000000..b3d280150c --- /dev/null +++ b/Insights/Host/GroupAdditions.yaml @@ -0,0 +1,149 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + let WellKnownLocalSID = 'S-1-5-32-5[0-9][0-9]$'; + let WellKnownGroupSID = 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-498$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1000$'; + let GetGroupAddForHost = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string){ + SecurityEvent + | where EventID in (4728, 4732, 4756) + // parsing for Host to handle variety of conventions coming from data + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | extend MemberAdded = case( MemberName has 'CN=', tostring(split(tostring(split(MemberName, ',')[0]),'CN=')[1]), MemberName == '-', MemberSid, MemberName) + | project TimeGenerated, EventID, Activity, Computer, MemberAdded, MemberName, MemberSid, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, SubjectUserName, SubjectUserSid, WellKnownGroupSID, WellKnownLocalSID, _ResourceId, SourceComputerId + | extend GroupName = TargetUserName, AddedBy = SubjectUserName + //support for Activities + | extend timestamp = TimeGenerated, HostCustomEntity = Computer + }; + GetGroupAddForHost('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +# The queries for the insights. +Insights: + Id: 44c9d0fe-c131-4080-a34f-da0e349da336 + DisplayName: Group additions + Description: | + Summary of user additions to Groups on the specific host. Specifically, we provide the count of All Groups, [Privileged Groups](https://docs.microsoft.com/windows/security/identity-protection/access-control/active-directory-security-groups) and Remote Desktop Users Group. + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: "Groups" + OutputType: String + SupportDeepLink: false + - Header: GroupCount + OutputType: Number + SupportDeepLink: true + - Header: UsersAddedCount + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + + # UserAddedToPrivilegedGroups + - Filter: "where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID" + Summarize: "summarize GroupCount = dcount(GroupName), UsersAddedCount = dcount(MemberAdded) by Computer" + Project: "project Title = 'Privileged', GroupCount, UsersAddedCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UsersAddedCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserAddedToRemoteDesktopGroup + - Filter: "where TargetSid in ('S-1-5-32-555')" + Summarize: "summarize GroupCount = dcount(GroupName), UsersAddedCount = dcount(MemberAdded)" + Project: "project Title = 'Remote Desktop', GroupCount, UsersAddedCount by Computer" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UserAddedToPrivilegedGroupsExcludeRDP + - Filter: "where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID | where TargetSid !in ('S-1-5-32-555')" + Summarize: "summarize GroupCount = dcount(GroupName), UsersAddedCount = dcount(MemberAdded) by Computer" + Project: "project Title = 'Privileged(non-RDP)', GroupCount, UsersAddedCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # UsersAddedToGroups + - Filter: "order by GroupName" + Summarize: "summarize GroupCount = dcount(GroupName), UsersAddedCount = dcount(MemberAdded) by Comptuer" + Project: "project Title = 'All', GroupCount, UsersAddedCount" + LinkColumnsDefinitions: + - ProjectedName: GroupCount + Query: "{{BaseQuery}}" + + ChartQuery: + Title: "Group additions per hour" + DataSets: + - Query: "summarize Count = count() by bin(TimeGenerated, 1h) | extend Legend = 'Total'" + XColumnName: "TimeGenerated" + YColumnName: "Count" + LegendColumnName: "Legend" + Type: LineChart + + AdditionalQuery: + Text: "See all group additions" + Query: "project TimeGenerated, EventID, Activity, Computer, MemberAdded, MemberName, MemberSid, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, SubjectUserName, SubjectUserSid, WellKnownGroupSID, WellKnownLocalSID, _ResourceId, SourceComputerId, timestamp, HostCustomEntity | order by TimeGenerated desc" + +Activities: + EnabledByDefault: true + Items: + - Id: b880ad94-f905-4ba8-8a3f-9088b19b12fa + Description: Account added to local Administrators group + Title: "An account was added to the local Administrators group" + Content: "On '{{Computer}}' the user '{{MemberAdded}}' was added by '{{AddedBy}}' to group: '{{GroupName}}'" + QueryDefinitions: + Filter: where TargetSid == 'S-1-5-32-544' + SummarizeBy: Computer + - Id: aaad22c3-be50-465f-b258-8570d629c3db + Description: Account added to the Domain Admins group + Title: "An account was added to the Domain Admins group" + Content: "On '{{Computer}}' the user '{{MemberAdded}}' was added by '{{AddedBy}}' to group: '{{GroupName}}'" + QueryDefinitions: + Filter: where TargetSid matches regex 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-512$' + SummarizeBy: Computer + - Id: cf3469b3-f64c-4ae2-9900-289617443d74 + Description: Account added to the Enterprise Admins group + Title: "An account was added to the Enterprise Admins group" + Content: "On '{{Computer}}' the user '{{MemberAdded}}' was added by '{{AddedBy}}' to group: '{{GroupName}}'" + QueryDefinitions: + Filter: where TargetSid matches regex 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-519$' + SummarizeBy: Computer + - Id: 5ba7b064-c667-4bb9-b8ac-7e87872ae479 + Description: Account added to privileged group. + Title: "Account added to a privileged group" + Content: "On '{{Computer}}' the user '{{MemberAdded}}' was added by '{{AddedBy}}' to group: '{{GroupName}}'" + QueryDefinitions: + Filter: where (TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID) and TargetSid != 'S-1-5-32-544' and not(TargetSid matches regex 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-512$') and not(TargetSid matches regex 'S-1-5-21-[0-9]*-[0-9]*-[0-9]*-519$') + SummarizeBy: MemberAdded, AddedBy, GroupName diff --git a/Insights/Host/LinuxActionsOnAccounts.yaml b/Insights/Host/LinuxActionsOnAccounts.yaml new file mode 100644 index 0000000000..702877cc59 --- /dev/null +++ b/Insights/Host/LinuxActionsOnAccounts.yaml @@ -0,0 +1,86 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: Syslog +EntitiesFilter: + Host_OsFamily: + - Linux +RequiredInputFieldsSets: + - - Host_HostName + - - Host_AzureID +BaseQuery: | + let AllUserEvents = (v_Host_Name:string, v_Host_AzureID:string) { + Syslog + | where Computer == v_Host_Name or v_Host_AzureID == _ResourceId + | where Facility == 'authpriv' + | where ProcessName in~ ('useradd','userdel') + | where SyslogMessage startswith 'new user:' or SyslogMessage startswith 'delete user ' + | extend User = case(SyslogMessage startswith 'new user:', tostring(split(tostring(split(SyslogMessage, 'name=')[1]), ',')[0]), + SyslogMessage startswith 'delete user ', tostring(split(SyslogMessage, "'")[1]), + 'Not Available') + | extend Action = case( SyslogMessage startswith 'new user', 'new user', SyslogMessage startswith 'delete user', 'delete user', 'None') + | project TimeGenerated, Computer, HostIP, User, Facility, ProcessName, Action, SyslogMessage, _ResourceId + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, IPCustomEntity = HostIP, AccountCustomEntity = User + }; + AllUserEvents('{{Host_HostName}}', '{{Host_AzureID}}') +Insights: + Id: e7144614-84b3-4884-bc14-cba1b9bac0de + DisplayName: Linux new or deleted users on host + Description: | + 'Summary of actions taken by Sudo on the specified host grouped by newly created or deleted users' + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: Action + OutputType: String + - Header: User + OutputType: String + - Header: UserCount + OutputType: Number + SupportDeepLink: true + - Header: ActionCount + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + # UsersCreated + - Filter: "where Action == 'new user'" + Summarize: "summarize UserCount = dcount(User), NewUsers = makeset(User), ActionCount = count() by Computer, Action" + Project: "project Action, User = case(UserCount == 1, tostring(NewUsers[0]), UserCount > 1, 'Many', 'None'), UserCount, ActionCount" + # UsersDeleted + - Filter: "where Action == 'delete user'" + Summarize: "summarize UserCount = dcount(User), DelUsers = makeset(User), ActionCount = count() by Computer, Action" + Project: "project Action, User = case(UserCount == 1, tostring(DelUsers[0]), UserCount > 1, 'Many', 'None'), UserCount, ActionCount" + + ChartQuery: + Title: "New or deleted users over time" + DataSets: + - Query: "summarize SudoUsage = count(User) by Time = bin(TimeGenerated, 1h), Action | extend Legend = Action" + XColumnName: Time + YColumnName: SudoUsage + LegendColumnName: Legend + Type: BarChart + AdditionalQuery: + Text: "See all new or deleted users" + Query: "project TimeGenerated, Computer, HostIP, User, Facility, ProcessName, Action, SyslogMessage, _ResourceId, timestamp, HostCustomEntity, AccountCustomEntity, IPCustomEntity" + +Activities: + EnabledByDefault: true + Items: + - Id: 290032e9-c52e-4e66-841a-7428f0b356bb + Description: Account created on Host + Title: "An account was created on this host" + Content: "On '{{Computer}}' the account '{{User}}' was created by sudo" + QueryDefinitions: + Filter: where Action == 'new user' + SummarizeBy: Computer + - Id: ce9e87c7-2ffa-42cb-92e5-f1a4f21f007a + Description: Account deleted on Host + Title: "An account was deleted on this host" + Content: "On '{{Computer}}' the account '{{User}}' was deleted by sudo" + QueryDefinitions: + Filter: where Action == 'delete user' + SummarizeBy: Computer \ No newline at end of file diff --git a/Insights/Host/LinuxGroupActions.yaml b/Insights/Host/LinuxGroupActions.yaml new file mode 100644 index 0000000000..cd9c36e2e0 --- /dev/null +++ b/Insights/Host/LinuxGroupActions.yaml @@ -0,0 +1,115 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: Syslog +EntitiesFilter: + Host_OsFamily: + - Linux +RequiredInputFieldsSets: + - - Host_HostName + - - Host_AzureID +BaseQuery: | + let AllUserEvents = (v_Host_Name:string, v_Host_AzureID:string) { + Syslog + | where Computer == v_Host_Name or v_Host_AzureID == _ResourceId + | where Facility == 'authpriv' + | where SyslogMessage !startswith "omsagent" + | where SyslogMessage has 'COMMAND' or ProcessName in~ ('gpasswd', 'useradd', 'userdel') + | parse SyslogMessage with * 'user ' User ' ' Verb ' by ' AcctMakingChange ' ' Preposition ' group ' Group + | extend Group = case( + SyslogMessage startswith 'removed group' or SyslogMessage startswith 'removed shadow group', tostring(split(SyslogMessage, "'")[1]), + SyslogMessage startswith 'new group', tostring(split(tostring(split(SyslogMessage, '=')[1]),',')[0]), + Group) + | extend Action = case( + isnotempty(Verb) or isnotempty(Preposition), strcat(Verb, ' ', Preposition), + SyslogMessage startswith 'new group', 'new group', + SyslogMessage startswith 'removed group', 'removed group', + SyslogMessage startswith 'removed shadow group', 'removed shadow group', + 'None') + | where isnotempty(Action) and Action != 'None' and isnotempty(Group) + | project TimeGenerated, Computer, HostIP, User, Action, Group, Facility, ProcessName, AcctMakingChange, SyslogMessage, _ResourceId + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, IPCustomEntity = HostIP, AccountCustomEntity = User + }; + AllUserEvents('{{Host_HostName}}', '{{Host_AzureID}}') +Insights: + Id: f1607751-8784-4a69-a91b-45b56683bc77 + DisplayName: Linux group actions on host + Description: | + 'Summary of additions or removals to groups by Sudo on the specified host, specifically the count for Sudo Group, Any group, Group creations and deletions' + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: Action + OutputType: String + - Header: Group(s) + OutputType: String + - Header: GroupCount + OutputType: Number + SupportDeepLink: true + - Header: UserCount + OutputType: Number + SupportDeepLink: true + - Header: User(s) + OutputType: String + QueriesDefinitions: + # UsersAddedtoSudoGroup + - Filter: "where Action =~ 'added to' and Group =~ 'sudo' | extend Action = strcat('users ', Action)" + Summarize: "summarize UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = 'Sudo', GroupCount = 1, UserCount, Users = case(UserCount == 1, tostring(UsersAdded[0]), UserCount > 1, 'Many', 'None')" + # UsersRemovedFromSudoGroup + - Filter: "where Action =~ 'removed from' and Group =~ 'sudo' | extend Action = strcat('users ', Action)" + Summarize: "summarize UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = 'Sudo', GroupCount = 1, UserCount, Users = case(UserCount == 1, tostring(UsersAdded[0]), UserCount > 1, 'Many', 'None')" + # UsersAddedtoAnyGroup + - Filter: "where Action =~ 'added to' | extend Action = strcat('users ', Action)" + Summarize: "summarize GroupCount = dcount(Group), UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = 'Any', GroupCount, UserCount, Users = case(UserCount == 1, tostring(UsersAdded[0]), UserCount > 1, 'Many', 'None')" + # UsersRemovedFromAnyGroup + - Filter: "where Action =~ 'removed from' | extend Action = strcat('users ', Action)" + Summarize: "summarize GroupCount = dcount(Group), UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = 'Any', GroupCount, UserCount, Users = case(UserCount == 1, tostring(UsersAdded[0]), UserCount > 1, 'Many', 'None')" + # GroupAdded + - Filter: "where Action =~ 'new group' | extend Action = strcat(Action, 's created')" + Summarize: "summarize Groups = make_set(Group), GroupCount = dcount(Group), UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = case(GroupCount == 1, tostring(Groups[0]), GroupCount > 1, 'Many', 'None'), GroupCount, UserCount = 0, Users = 'None'" + # GroupDeleted + - Filter: "where Action =~ 'removed group'" + Summarize: "summarize Groups = make_set(Group), GroupCount = dcount(Group), UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = case(GroupCount == 1, tostring(Groups[0]), GroupCount > 1, 'Many', 'None'), GroupCount, UserCount = 0, Users = 'None'" + # ShadowGroupDeleted + - Filter: "where Action =~ 'removed shadow group'" + Summarize: "summarize Groups = make_set(Group), GroupCount = dcount(Group), UserCount = dcount(User), UsersAdded = makeset(User) by Computer, Action" + Project: "project Action, Groups = case(GroupCount == 1, tostring(Groups[0]), GroupCount > 1, 'Many', 'None'), GroupCount, UserCount = 0, Users = 'None'" + ChartQuery: + Title: "Group actions over time" + DataSets: + - Query: "summarize SudoUsage = count(User) by Time = bin(TimeGenerated, 1h), Action | extend Legend = Action" + XColumnName: Time + YColumnName: SudoUsage + LegendColumnName: Legend + Type: BarChart + AdditionalQuery: + Text: "See all group actions" + Query: "project TimeGenerated, Computer, HostIP, User, Action, Group, Facility, ProcessName, AcctMakingChange, SyslogMessage, _ResourceId, timestamp, HostCustomEntity, AccountCustomEntity, IPCustomEntity" + +Activities: + EnabledByDefault: true + Items: + - Id: 46aeae2d-187c-41f9-b8d6-9d75c43bce0a + Description: Account added to the sudo group + Title: "An account was added to the sudo group" + Content: "On '{{Computer}}' the user '{{User}}' was added by '{{AcctMakingChange}}' to group: '{{Group}}'" + QueryDefinitions: + Filter: where Action =~ 'added to' and Group =~ 'sudo' + SummarizeBy: Computer + - Id: e24dd437-c65e-40e1-8d59-cd303ad4496a + Description: Account removed from sudo group + Title: "An account was removed from the sudo group" + Content: "On '{{Computer}}' the user '{{User}}' was added by '{{AcctMakingChange}}' to group: '{{Group}}'" + QueryDefinitions: + Filter: where Action =~ 'removed from' and Group =~ 'sudo' + SummarizeBy: Computer \ No newline at end of file diff --git a/Insights/Host/LinuxSigninActivity.yaml b/Insights/Host/LinuxSigninActivity.yaml new file mode 100644 index 0000000000..f2fa6d486b --- /dev/null +++ b/Insights/Host/LinuxSigninActivity.yaml @@ -0,0 +1,110 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: Syslog +EntitiesFilter: + Host_OsFamily: + - Linux +RequiredInputFieldsSets: + - - Host_HostName + - - Host_AzureID +BaseQuery: | + let SigninResults = + Syslog + | where Facility =~ 'auth' + | where SyslogMessage startswith 'Accepted' or SyslogMessage startswith 'Failed'; + let AllSigninResults = (v_Host_Name:string, v_Host_AzureID:string) + { + let HostSpecificResults = SigninResults + | where Computer == v_Host_Name or v_Host_AzureID == _ResourceId; + let AcceptedAuth = HostSpecificResults + | where SyslogMessage startswith 'Accepted'; + let LongAuth = AcceptedAuth + | where SyslogMessage has ':' + | parse SyslogMessage with * 'Accepted ' LogonMethod ' for ' User ' from ' ExternalIP ' port ' Port ' ' ConnectionType ':' TrimExtra + | project TimeGenerated = EventTime, HostName, HostIP, User, LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result = 'SuccessfulSignin', _ResourceId; + let ShortAuth = AcceptedAuth + | where SyslogMessage !has ':' + | parse SyslogMessage with * 'Accepted ' LogonMethod ' for ' User ' from ' ExternalIP ' port ' Port ' ' ConnectionType + | project TimeGenerated = EventTime, HostName, HostIP, User, LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result = 'SuccessfulSignin', _ResourceId; + let InitialFailedAuth = HostSpecificResults + | where SyslogMessage startswith 'Failed'; + let FailedAuth = InitialFailedAuth + | where SyslogMessage !has 'invalid' + | parse SyslogMessage with * 'Failed ' LogonMethod ' for ' User ' from ' ExternalIP ' port ' Port ' ' ConnectionType + | project TimeGenerated = EventTime, HostName, HostIP, User, LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result = 'FailedSignin', _ResourceId; + let ShortInvalidAuth = InitialFailedAuth + | where SyslogMessage has 'invalid user from' + | parse SyslogMessage with * 'Failed ' LogonMethod ' for invalid user from ' ExternalIP ' port ' Port ' ' ConnectionType + | project TimeGenerated = EventTime, HostName, HostIP, User = ' ', LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result = 'InvalidSignin', _ResourceId; + let LongInvalidAuth = InitialFailedAuth + | where SyslogMessage has 'invalid' and SyslogMessage !has ' user from' + | parse SyslogMessage with * 'Failed ' LogonMethod ' for invalid user ' User ' from ' ExternalIP ' port ' Port ' ' ConnectionType + | project TimeGenerated = EventTime, HostName, HostIP, User, LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result = 'InvalidSignin', _ResourceId; + union isfuzzy=true LongAuth, ShortAuth, FailedAuth, ShortInvalidAuth, LongInvalidAuth + | extend timestamp = TimeGenerated, HostCustomEntity = HostName, IPCustomEntity = HostIP, AccountCustomEntity = User + }; + AllSigninResults('{{Host_HostName}}', '{{Host_AzureID}}') +Insights: + Id: d4ca45db-254b-46f0-98fa-d1d104c26e0c + DisplayName: Linux sign-in activity + Description: | + 'Summary of successful, failed or invalid signins, along with most frequent and least frequent signins.' + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: "Signin Result" + OutputType: String + - Header: "Signin Count" + OutputType: Number + SupportDeepLink: true + - Header: "User Count" + OutputType: Number + SupportDeepLink: true + - Header: "User(s)" + OutputType: String + QueriesDefinitions: + # UserSigninsToHost + - Filter: "where Result == 'SuccessfulSignin'" + Summarize: "summarize UserCount = dcount(User), Users = makeset(User), SigninCount = count() by HostName" + Project: "project Title = 'Success', SigninCount = case(UserCount == 0, 0, isempty(SigninCount), 0, SigninCount), UserCount, Users = case(UserCount == 1, tostring(Users[0]), UserCount > 1, 'Many', 'None')" + # UserFailedSigninsToHost + - Filter: "where Result == 'FailedSignin'" + Summarize: "summarize UserCount = dcount(User), Users = makeset(User), SigninCount = count() by HostName" + Project: "project Title = 'Fail', SigninCount = case(UserCount == 0, 0, isempty(SigninCount), 0, SigninCount), UserCount, Users = case(UserCount == 1, tostring(Users[0]), UserCount > 1, 'Many', 'None')" + # InvalidSigninsToHost + - Filter: "where Result == 'InvalidSignin'" + Summarize: "summarize UserCount = dcount(User), Users = makeset(User), SigninCount = count() by HostName" + Project: "project Title = 'Invalid', SigninCount = case(UserCount == 0, 0, isempty(SigninCount), 0, SigninCount), UserCount, Users = case(UserCount == 1, tostring(Users[0]), UserCount > 1, 'Many', 'None')" + # MostFrequent + - Filter: "where Result == 'SuccessfulSignin'" + Summarize: "summarize StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SigninCount = count() by User | top 1 by SigninCount desc" + Project: "project Title = 'Most Frequent', SigninCount, UserCount = 1, Users = User" + # LeastFrequent + - Filter: "where Result == 'SuccessfulSignin'" + Summarize: "summarize StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SigninCount = count() by User | top 1 by SigninCount asc" + Project: "project Title = 'Least Frequent', SigninCount, UserCount = 1, Users = User" + + ChartQuery: + Title: "Sign-ins over time" + DataSets: + - Query: "where Result == 'SuccessfulSignin' | summarize SigninCount = count() by Time = bin(TimeGenerated, 1h) | extend Legend = 'Success'" + XColumnName: Time + YColumnName: SigninCount + LegendColumnName: Legend + - Query: "where Result == 'FailedSignin' | summarize SigninCount = count() by Time = bin(TimeGenerated, 1h) | extend Legend = 'Fail'" + XColumnName: Time + YColumnName: SigninCount + LegendColumnName: Legend + - Query: "where Result == 'InvalidSignin' | summarize SigninCount = count() by Time = bin(TimeGenerated, 1h) | extend Legend = 'Invalid'" + XColumnName: Time + YColumnName: SigninCount + LegendColumnName: Legend + Type: LineChart + AdditionalQuery: + Text: "See all Linux sign-ins" + Query: "summarize StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SigninCount = count() by HostName, HostIP, User, LogonMethod, ExternalIP, Port, ConnectionType, ProcessName, Result, _ResourceId, timestamp, HostCustomEntity, AccountCustomEntity, IPCustomEntity" \ No newline at end of file diff --git a/Insights/Host/LinuxSudoEvents.yaml b/Insights/Host/LinuxSudoEvents.yaml new file mode 100644 index 0000000000..7348de71d7 --- /dev/null +++ b/Insights/Host/LinuxSudoEvents.yaml @@ -0,0 +1,83 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: Syslog +EntitiesFilter: + Host_OsFamily: + - Linux +RequiredInputFieldsSets: + - - Host_HostName + - - Host_AzureID +BaseQuery: | + let AllUserEvents = (v_Host_Name:string, v_Host_AzureID:string) { + Syslog + | where Computer == v_Host_Name or v_Host_AzureID == _ResourceId + | where Facility == "authpriv" + | where SyslogMessage !startswith "omsagent" + | where SyslogMessage has 'COMMAND' + | parse SyslogMessage with User ' : TTY=' TTY ' PWD=' WorkingDirectory ' USER=' CmdRunAs ' COMMAND=' Commandline + | where User != 'omsagent' + | parse Commandline with Command ' ' * + | extend Command = case(isempty(Command), Commandline, Command) + | project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage, _ResourceId + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, IPCustomEntity = HostIP, AccountCustomEntity = User + }; + AllUserEvents('{{Host_HostName}}', '{{Host_AzureID}}') +Insights: + Id: a9191fbe-ca33-400c-8036-18caac59271c + DisplayName: Linux sudo usage on host + Description: | + 'Sudo usage on host by users, most/least events by commands, most/least events by user' + DefaultTimeRange: + BeforeRange: 7d + AfterRange: 7d + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: "Sudo Usage" + OutputType: String + - Header: User + OutputType: String + - Header: "Usage Count" + OutputType: Number + SupportDeepLink: true + - Header: Command(s) + OutputType: String + SupportDeepLink: true + - Header: "Distinct Commands" + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + # MostSudoEventsByCommand + - Filter: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage" + Summarize: "summarize User = make_set(User), UserCount = dcount(User), UsageCount = count() by Command | top 1 by UsageCount desc" + Project: "project Title = 'Most by Command', User = case(UserCount == 1, tostring(User[0]), UserCount > 1, 'Many', 'None'), UsageCount = case(UsageCount == 0, 0, UsageCount), Commands = Command, CommandCount = 1" + # LeastSudoEventsByCommand + - Filter: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage" + Summarize: "summarize User = make_set(User), UserCount = dcount(User), UsageCount = count() by Command | top 1 by UsageCount asc" + Project: "project Title = 'Least by Command', User = case(UserCount == 1, tostring(User[0]), UserCount > 1, 'Many', 'None'), UsageCount = case(UsageCount == 0, 0, UsageCount), Commands = Command, CommandCount = 1" + # MostSudoEventsByUser + - Filter: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage" + Summarize: "summarize CommandCount = dcount(Command), Commands = make_set(Command), UsageCount = count() by User | top 1 by UsageCount desc" + Project: "project Title = 'Most by User', User = case(isnotempty(User), User, 'None'), UsageCount = case(UsageCount == 0, 0, UsageCount), Commands = case(CommandCount == 1, tostring(Commands[0]), CommandCount > 1, 'Many', 'None'), CommandCount = case(CommandCount == 0, 0, CommandCount)" + # LeastSudoEventsByUser + - Filter: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage" + Summarize: "summarize CommandCount = dcount(Command), Commands = make_set(Command),UsageCount = count() by User | top 1 by UsageCount asc" + Project: "project Title = 'Least by User', User = case(isnotempty(User), User, 'None'), UsageCount = case(UsageCount == 0, 0, UsageCount), Commands = case(CommandCount == 1, tostring(Commands[0]), CommandCount > 1, 'Many', 'None'), CommandCount = case(CommandCount == 0, 0, CommandCount)" + # AllSudoEvents + - Filter: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage" + Summarize: "summarize CommandCount = dcount(Command), Commands = make_set(Command), User = make_set(User), UserCount = dcount(User), UsageCount = count() by Computer" + Project: "project Title = 'All', User = case(UserCount == 1, tostring(User[0]), UserCount > 1, 'Many', 'None'), UsageCount = case(UsageCount == 0, 0, UsageCount), Commands = case(CommandCount == 1, tostring(Commands[0]), CommandCount > 1, 'Many', 'None'), CommandCount = case(CommandCount == 0, 0, CommandCount)" + + ChartQuery: + Title: "Sudo usage over time" + DataSets: + - Query: "summarize UsageCount = count(User) by Time = bin(TimeGenerated, 1h), Command | extend Legend = Command" + XColumnName: Time + YColumnName: UsageCount + LegendColumnName: Legend + Type: BarChart + AdditionalQuery: + Text: "See all sudo usage" + Query: "project TimeGenerated, Computer, HostIP, User, CmdRunAs, WorkingDirectory, Command, Commandline, SyslogMessage, _ResourceId, timestamp, HostCustomEntity, AccountCustomEntity, IPCustomEntity" \ No newline at end of file diff --git a/Insights/Host/ProcessEntropy.yaml b/Insights/Host/ProcessEntropy.yaml new file mode 100644 index 0000000000..8d3e545f75 --- /dev/null +++ b/Insights/Host/ProcessEntropy.yaml @@ -0,0 +1,199 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + // exclude when over # of machines have the process + let excludeThreshold = 10; + // exclude when more than percent (default 10%) + let ratioHighCount = 0.1; + // exclude when less than percent (default 3%) + let ratioMidCount = 0.03; + // Process count limit in one day, perf improvement (default every minute for 24 hours - 60*24 = 1440) + let procLimit = 60*24; + let SecEvents = SecurityEvent + | where EventID == 4688; + let EntropyCalc = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string) + { + let Exclude = SecEvents + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ExcludeCompCount = dcount(Computer), ExcludeProcCount = count() by Process + // Removing general limit for noise in one day + | extend timediff = iff(datetime_diff('day', EndTime, StartTime) > 0, datetime_diff('day', EndTime, StartTime), 1) + // Default exclude of 1440 (1 per min) or more executions in 24 hours on a given machine + | where ExcludeProcCount < procLimit*timediff + // Removing noisy processes for an environment, adjust as needed + | extend compRatio = ExcludeCompCount/toreal(ExcludeProcCount) + | where compRatio == 0 or (ExcludeCompCount > excludeThreshold and compRatio < ratioHighCount) or (ExcludeCompCount between (2 .. excludeThreshold) and compRatio < ratioMidCount); + let AllSecEvents = SecEvents + // Removing general limit for noise in one day + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), procCount = count() by Computer, Process + | extend timediff = iff(datetime_diff('day', EndTime, StartTime) > 0, datetime_diff('day', EndTime, StartTime), 1) + // Default include the 1440 (1 per min) or more executions in 24 hours on a given machine to remove them from overall comparison list + | where procCount > procLimit*timediff + | join kind= rightanti ( + SecEvents | project Computer, Process + ) on Computer, Process + | project Computer, Process; + // Removing noisy process from full list + let Include = materialize(Exclude + | join kind= rightanti ( + AllSecEvents + ) on Process); + // Identifying prevalence for a given process in the environment + let DCwPC = materialize(Include + | summarize DistinctComputersWithProcessCount = dcount(Computer) by Process + | join kind=inner ( + Include + ) on Process + | distinct Computer, Process, DistinctComputersWithProcessCount); + // Getting the Total process count on each host to use as the denominator in the entropy calc + let TPCoH = materialize(Include + | summarize TotalProcessCountOnHost = count(Process) by Computer + | join kind=inner ( + Include + ) on Computer + | distinct Computer, Process, TotalProcessCountOnHost + //Getting a decimal value for later computation + | extend TPCoHValue = todecimal(TotalProcessCountOnHost)); + // Need the count of each class in my bucket or also said as count of ProcName(Class) per Host(Bucket) for use in the entropy calc + let PCoH = Include + | summarize ProcessCountOnHost = count(Process) by Computer, Process + | join kind=inner ( + Include + ) on Computer,Process + | distinct Computer, Process, ProcessCountOnHost + //Getting a decimal value for later computation + | extend PCoHValue = todecimal(ProcessCountOnHost); + let Combined = DCwPC + | join ( + TPCoH + ) on Computer, Process + | join ( + PCoH + ) on Computer, Process; + let Results = Combined + // Entropy calculation + | extend ProcessEntropy = -log2(PCoHValue/TPCoHValue)*(PCoHValue/TPCoHValue) + | extend NormalizedProcessEntropy = toreal(ProcessEntropy*10000) + // Calculating Weight, see details in description + | extend Weight = toreal((ProcessEntropy*10000)*ProcessCountOnHost*DistinctComputersWithProcessCount) + // Remove or increase value to see processes with low entropy, meaning more common. + | where Weight <= 1000 + | project Computer, Process, Weight , ProcessEntropy, TotalProcessCountOnHost, ProcessCountOnHost, DistinctComputersWithProcessCount, NormalizedProcessEntropy; + // Join back full entry + Results + | join kind= inner ( + SecEvents + | project TimeGenerated, EventID, Computer, SubjectUserSid, Account, AccountType, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId, SourceComputerId + ) on Computer, Process + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + // removing common items that may still show up in small environments, add here is you have additional exclusions + | where NewProcessName !endswith ':\\Windows\\System32\\conhost.exe' and ParentProcessName !endswith ':\\Windows\\System32\\conhost.exe' + | where ParentProcessName !endswith ':\\Windows\\System32\\wuauclt.exe' and NewProcessName !endswith ':\\Windows\\System32\\wuauclt.exe' and NewProcessName !startswith 'C:\\Windows\\SoftwareDistribution\\Download\\Install\\AM_Delta_Patch_' + | where ParentProcessName !has ':\\WindowsAzure\\GuestAgent_' and NewProcessName !has ':\\WindowsAzure\\GuestAgent_' + | where ParentProcessName !has ':\\WindowsAzure\\WindowsAzureNetAgent_' and NewProcessName !has ':\\WindowsAzure\\WindowsAzureNetAgent_' + | where ParentProcessName !has ':\\ProgramData\\Microsoft\\Windows Defender\\platform\\' + | where NewProcessName !has ':\\ProgramData\\Microsoft\\Windows Defender\\platform\\' + | where NewProcessName !has ':\\Windows\\Microsoft.NET\\Framework' and not(NewProcessName endswith '\\ngentask.exe' or NewProcessName endswith '\\ngen.exe') and not(ParentProcessName endswith ':\\Windows\\System32\\taskhostw.exe') + | where NewProcessName !endswith '\\MpSigStub.exe' and ParentProcessName !has ':\\Windows\\SoftwareDistribution\\Download\\Install\\' + | where ParentProcessName !has ':\\Program Files\\Microsoft Monitoring Agent\\Agent\\MonitoringHost.exe' + | where NewProcessName !endswith ':\\Windows\\servicing\\trustedinstaller.exe' + | project TimeGenerated, EventID, Computer, SubjectUserSid, Account, AccountType, Weight, NormalizedProcessEntropy, FullDecimalProcessEntropy = ProcessEntropy, + Process, NewProcessName, CommandLine, ParentProcessName, TotalProcessCountOnHost, ProcessCountOnHost, DistinctComputersWithProcessCount, _ResourceId, SourceComputerId + | sort by Weight asc, NormalizedProcessEntropy asc, NewProcessName asc + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, AccountCustomEntity = Account + }; + EntropyCalc('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +Insights: + Id: 2b59ee1f-5098-4061-a868-e49c39ed2245 + DisplayName: Process rarity via entropy calculation + Description: | + Entropy calculation used to help identify Hosts where they have a high variety of processes(a high entropy process list on a given Host over time). + This helps us identify rare processes on a given Host. Rare here means a process shows up on the Host relatively few times in the the last 7days. + The Weight is calculated based on the Entropy, Process Count and Distinct Hosts with that Process. The lower the Weight/ProcessEntropy the, more interesting. + The Weight calculation increases the Weight if the process executes more than once on the Host or has executed on more than 1 Hosts. + In general, this should identify processes on a Host that are rare and rare for the environment. A Weight lower than 100 is considered very rare. + References: https://medium.com/udacity/shannon-entropy-information-gain-and-picking-balls-from-buckets-5810d35d54b4 + https://en.wiktionary.org/wiki/Shannon_entropy + DefaultTimeRange: + BeforeRange: 7d + AfterRange: 7d + TableQuery: + ColumnsDefinitions: + - Header: Label + OutputType: String + - Header: "Rare Process(es)" + OutputType: String + - Header: "Entropy Weight" + OutputType: Number + - Header: "Process Count" + OutputType: Number + SupportDeepLink: true + QueriesDefinitions: + + # TrulyRareProcess + - Filter: "extend Weight = round(Weight, 3) | where Weight <= 75" + Summarize: "summarize Weight = make_list(Weight), avgWeight = avg(round(Weight, 3)), Process = make_list(Process), ProcessCountOnHost = sum(ProcessCountOnHost) by Computer " + Project: "project Title = 'Rare Process(es)', Process = case(array_length(Process) > 1, 'Many', array_length(Process) == 1, tostring(Process[0]), 'None'), EntropyWeight = case(array_length(Weight) > 1, strcat(tostring(round(avgWeight, 3)), ' avg'), array_length(Weight) == 1, tostring(Weight[0]), 'None'), ProcessCountOnHost = iff(isempty(Process), 0, ProcessCountOnHost)" + + # LeastPrevalentProcesses + - Filter: "extend Weight = round(Weight, 3)" + Summarize: "sort by Weight asc, ProcessCountOnHost asc, TimeGenerated desc | take 3 | extend row = row_number()" + Project: "project Title = case(row == 1, strcat(row,'st Least prevalent'), row == 2, strcat(row,'nd Least prevalent'), row == 3, strcat(row,'rd Least prevalent'), 'None'), Process = case(row == 1, Process, row == 2, Process, row == 3, Process, 'None'), EntropyWeight = tostring(case(row == 1, Weight, row == 2, Weight, row == 3, Weight, 0.000)), ProcessCountOnHost = case(row == 1, ProcessCountOnHost, row == 2, ProcessCountOnHost, row == 3, ProcessCountOnHost, 0) | sort by Title" + + # ServiceParentProcesses + - Filter: "where ParentProcessName has_any (':\\\\windows\\\\system32\\\\svchost.exe',':\\\\windows\\\\system32\\\\services.exe')" + Summarize: "summarize Process = make_set(Process), Weight = make_set(round(Weight, 3)), avgWeight = avg(round(Weight, 3)), ProcessCountOnHost = count(Process) by Computer" + Project: "project Title = 'Services as Parent', Process = case(array_length(Process) > 1, 'Many', array_length(Process) == 1, tostring(Process[0]), 'None'), EntropyWeight = case(array_length(Weight) > 1, strcat(tostring(round(avgWeight, 3)), ' avg'), array_length(Weight) == 1, tostring(Weight[0]), 'None'), ProcessCountOnHost = iff(isempty(Process), 0, ProcessCountOnHost)" + + # MachineExecutions + - Filter: "where AccountType =~ 'machine'" + Summarize: "summarize Weight = make_set(round(Weight, 3)), avgWeight = avg(round(Weight, 3)), Process = make_set(Process), ProcessCountOnHost = count(Process) by Computer" + Project: "project Title = 'Execution by Machine Accounts', Process = case(array_length(Process) > 1, 'Many', array_length(Process) == 1, tostring(Process[0]), 'None'), EntropyWeight = case(array_length(Weight) > 1, strcat(tostring(round(avgWeight, 3)), ' avg'), array_length(Weight) == 1, tostring(Weight[0]), 'None'), ProcessCountOnHost = iff(isempty(Process), 0, ProcessCountOnHost)" + + # UserExecutions + - Filter: "where AccountType =~ 'user'" + Summarize: "summarize Weight = make_set(round(Weight, 3)), avgWeight = avg(round(Weight, 3)), Process = make_set(Process), ProcessCountOnHost = count(Process) by Computer" + Project: "project Title = 'Execution by User Accounts', Process = case(array_length(Process) > 1, 'Many', array_length(Process) == 1, tostring(Process[0]), 'None'), EntropyWeight = case(array_length(Weight) > 1, strcat(tostring(round(avgWeight, 3)), ' avg'), array_length(Weight) == 1, tostring(Weight[0]), 'None'), ProcessCountOnHost = iff(isempty(Process), 0, ProcessCountOnHost)" + + ChartQuery: + Title: "Rare processes over time (Weight <= 75)" + DataSets: + - Query: "where Weight <= 75 | summarize RareProcessCount = sum(ProcessCountOnHost) by Time = bin(TimeGenerated, 6h), Process" + XColumnName: Time + YColumnName: RareProcessCount + LegendColumnName: Process + Type: BarChart + AdditionalQuery: + Text: "See entropy weight levels 1000 and under" + Query: "where Weight <= 1000" \ No newline at end of file diff --git a/Insights/Host/SecurityEventsAnomaly.yaml b/Insights/Host/SecurityEventsAnomaly.yaml new file mode 100644 index 0000000000..494e0fe53b --- /dev/null +++ b/Insights/Host/SecurityEventsAnomaly.yaml @@ -0,0 +1,124 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Provider: Sentinel +Type: KQL +EntitiesFilter: + Host_OsFamily: + - Windows +BaseQuery: | + let AScoreThresh=3; + let maxAnomalies=3; + let BeforeRange = 14d; + let EndTime = todatetime('{{End_Time_UTC}}'); + let StartTime = todatetime('{{Start_Time_UTC}}'); + let numDays = tolong((EndTime-StartTime)/1d); + let computerData = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string) { + SecurityEvent + | extend SourceComputerId = column_ifexists("SourceComputerId", "NotAvailable"), _ResourceId = column_ifexists("_ResourceId", "NotAvailable") + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId }; + computerData('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +Insights: + Id: 4191a4d7-e72b-4564-b2fb-25580630384b + DisplayName: Anomalously high number of a security event + Description: Highlight security events of the host with anomalously high count compared to those observed in the preceding 14 days. + DefaultTimeRange: + BeforeRange: 1d + AfterRange: 0d + ReferenceTimeRange: + BeforeRange: 14d + TableQuery: + ColumnsDefinitions: + - Header: Activity + OutputType: String + SupportDeepLink: true + - Header: Expected Count + OutputType: Number + SupportDeepLink: false + - Header: Actual Count + OutputType: Number + SupportDeepLink: false + QueriesDefinitions: + - Filter: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Activity + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + + Summarize: take 1 + Project: project Activity, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) + LinkColumnsDefinitions: + - ProjectedName: Activity + Query: | + {{BaseQuery}} + | where TimeGenerated between (StartTime .. EndTime) + | where Activity == '{{RowValue_Activity}}' + ChartQuery: + Title: Anomalous activity timeline + DataSets: + - Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Activity + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,round(postExpectedCount,2)) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + | take 1 + | project Activity, TimeGenerated, count_ + | mvexpand TimeGenerated, count_ + | project todatetime(TimeGenerated), toint(count_), Activity + + XColumnName: TimeGenerated + YColumnName: count_ + LegendColumnName: Activity + Type: LineChart + AdditionalQuery: + Text: Query all anomalous activities + Query: | + make-series count() default=0 on TimeGenerated from (StartTime - BeforeRange) to EndTime step 1d by Activity + | extend (anomalies,anomalyScore, expectedCount)=series_decompose_anomalies(count_,AScoreThresh,7,'linefit',numDays, 'ctukey') + | extend count1=count_, TimeGenerated1=TimeGenerated, anomalyScore1=anomalyScore + | mv-apply count1 to typeof(long), TimeGenerated1 to typeof(datetime), anomalyScore1 to typeof(double), anomalies to typeof(long) on (summarize totAnomalies=sumif(abs(anomalies), TimeGenerated1 < StartTime), baseStd=stdevif(count1, TimeGenerated1 < StartTime), baseAvg=avgif(count1, TimeGenerated1 < StartTime), maxCountPost=maxif(count1,TimeGenerated1 >= StartTime), maxAnomalyScorePost = maxif(anomalyScore1, TimeGenerated1 >= StartTime)) + | extend count1=count_ + | mv-apply count1 to typeof(long), anomalyScore to typeof(double), expectedCount to typeof(double) on ( summarize (dummy, postExpectedCount, postActualCount)=arg_min(abs(anomalyScore - maxAnomalyScorePost), expectedCount, count1) ) + | where totAnomalies < maxAnomalies + | extend postAnomalyScore=iff(baseStd == 0 and maxCountPost > tolong(count_[0]),1000.0,maxAnomalyScorePost), postExpectedCount=iff(postExpectedCount < 0,0.0,postExpectedCount) + | where maxAnomalyScorePost > AScoreThresh + | order by maxAnomalyScorePost desc + | project Activity, expectedCount=round(postExpectedCount,2), actualCount=postActualCount, anomalyScore=round(postAnomalyScore,2) diff --git a/Insights/Host/WindowsProcessExecInfo.yaml b/Insights/Host/WindowsProcessExecInfo.yaml new file mode 100644 index 0000000000..53e16e81de --- /dev/null +++ b/Insights/Host/WindowsProcessExecInfo.yaml @@ -0,0 +1,121 @@ +SchemaVersion: '1.0' +Type: KQL +Provider: Sentinel +DataTypes: + - DataType: SecurityEvent +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + + let excludeProc = dynamic([':\\Windows\\System32\\svchost.exe', ':\\Windows\\System32\\sppsvc.exe', ':\\Windows\\system32\\wbem\\WmiApSrv.exe', ':\\Windows\\System32\\conhost.exe', ':\\Windows\\System32\\wuauclt.exe', ':\\Windows\\SoftwareDistribution\\Download\\Install\\', ':\\WindowsAzure\\GuestAgent_', ':\\WindowsAzure\\WindowsAzureNetAgent_', + ':\\ProgramData\\Microsoft\\Windows Defender\\platform\\', ':\\Windows\\System32\\taskhostw.exe', '\\MpSigStub.exe',':\\Program Files\\Microsoft Monitoring Agent\\Agent\\MonitoringHost.exe', ':\\Windows\\servicing\\trustedinstaller.exe', ':\\Windows\\System32\\WerFault.exe', ':\\Windows\\CCM\\CcmExec.exe']); + let starttime = todatetime('{{Start_Time_ISO}}'); + let endtime = todatetime('{{End_Time_ISO}}'); + let includeScope = 1d; + let historicalScope = 14d; + let Procs=(v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string){ + let processEvents=SecurityEvent + | where TimeGenerated >= (starttime - historicalScope) + | where EventID==4688 + // removing common items that may still show up in small environments, add here if you have additional exclusions + | where not(NewProcessName has_any (excludeProc)) and not(ParentProcessName has_any (excludeProc)) + // parsing for Host to handle variety of conventions coming from data + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | project TimeGenerated, Computer, SubjectAccount, NewProcessName, Process, CommandLine, ParentProcessName, ParentProcess = tostring(split(ParentProcessName, '\\')[-1]), _ResourceId, SourceComputerId; + let processes = materialize(processEvents + | where TimeGenerated >= endtime - includeScope + | join kind=leftanti ( + processEvents + | where TimeGenerated between ((starttime - historicalScope) .. (endtime - includeScope)) + | distinct NewProcessName + ) on NewProcessName + | extend ExecType = 'Processes'); + let parents = materialize(processEvents + | where TimeGenerated >= endtime - includeScope + | join kind=leftanti ( + processEvents + | where TimeGenerated between ((starttime - historicalScope) .. (endtime - includeScope)) + | distinct ParentProcessName + ) on ParentProcessName + | extend ExecType = 'Parents'); + union isfuzzy=true processes, parents + | extend timestamp = TimeGenerated, HostCustomEntity = Computer, AccountCustomEntity = SubjectAccount + }; + Procs('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +Insights: + Id: 37a420dc-8d03-4a1f-b579-d96d5c5f5fe4 + DisplayName: Windows process execution info + Description: | + 'Identifies new processes and new parent processes, along with low or high execution counts.' + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + SingleValuesQuery: {} + TableQuery: + ColumnsDefinitions: + - Header: ProcessType + OutputType: String + - Header: ProcessName + OutputType: Number + SupportDeepLink: true + - Header: Executions + OutputType: Number + SupportDeepLink: true + - Header: User(s) + OutputType: String + QueriesDefinitions: + # NewProcessExec + - Filter: "where ExecType == 'Processes'" + Summarize: "summarize UserCount = dcount(SubjectAccount), Users = make_set(SubjectAccount), Processes = make_set(Process), ProcCount = count()" + Project: "project Title = 'New Process(es)', Processes = case(array_length(Processes) == 1, tostring(Processes[0]), array_length(Processes) > 1, 'Many', 'None'), ProcCount, Users = case(array_length(Users) == 1, tostring(Users[0]), array_length(Users) > 1, 'Many', 'None')" + # NewParentProcessExec + - Filter: "where ExecType == 'Parents'" + Summarize: "summarize UserCount = dcount(SubjectAccount), Users = make_set(SubjectAccount), Processes = make_set(ParentProcess), ProcCount = count()" + Project: "project Title = 'New Parent(s)', Processes = case(array_length(Processes) == 1, tostring(Processes[0]), array_length(Processes) > 1, 'Many', 'None'), ProcCount, Users = case(array_length(Users) == 1, tostring(Users[0]), array_length(Users) > 1, 'Many', 'None')" + # LeastNewProcessExecs + - Filter: "where ExecType == 'Processes' | project TimeGenerated, Computer, SubjectAccount, Process" + Summarize: "summarize UserCount = dcount(SubjectAccount), Users = make_set(SubjectAccount), ProcCount = count() by Process | order by ProcCount, UserCount asc | top 1 by ProcCount" + Project: "project Title = 'Least New Process Executions', Processes = Process, ProcCount, Users = case(array_length(Users) == 1, tostring(Users[0]), array_length(Users) > 1, 'Many', 'None')" + # MostNewProcessExecs + - Filter: "where ExecType == 'Processes' | project TimeGenerated, Computer, SubjectAccount, Process" + Summarize: "summarize UserCount = dcount(SubjectAccount), Users = make_set(SubjectAccount), ProcCount = count() by Process | order by ProcCount, UserCount desc | top 1 by ProcCount" + Project: "project Title = 'Most New Process Executions', Processes = Process, ProcCount, Users = case(array_length(Users) == 1, tostring(Users[0]), array_length(Users) > 1, 'Many', 'None')" + + ChartQuery: + Title: "Process executions over time" + DataSets: + - Query: "summarize ProcessCount = count() by Time = bin(TimeGenerated, 1h), Process | extend Legend = Process" + XColumnName: Time + YColumnName: ProcessCount + LegendColumnName: Legend + Type: BarChart + AdditionalQuery: + Text: "See all new process information" + Query: "project TimeGenerated, Computer, SubjectAccount, NewProcessName, Process, CommandLine, ParentProcessName, ParentProcess, ExecType, _ResourceId, SourceComputerId, timestamp, HostCustomEntity, AccountCustomEntity" \ No newline at end of file diff --git a/Insights/Host/WindowsSigninActivity.yaml b/Insights/Host/WindowsSigninActivity.yaml new file mode 100644 index 0000000000..84e704bb62 --- /dev/null +++ b/Insights/Host/WindowsSigninActivity.yaml @@ -0,0 +1,271 @@ +SchemaVersion: 1.0 +DataTypes: + - DataType: SecurityEvent +Type: KQL +Provider: Sentinel +EntitiesFilter: + Host_OsFamily: + - Windows +RequiredInputFieldsSets: + - - Host_HostName + - Host_NTDomain + - - Host_HostName + - Host_DnsDomain + - - Host_AzureID + - - Host_OMSAgentID +BaseQuery: | + let starttime = todatetime('{{Start_Time_ISO}}'); + let endtime = todatetime('{{End_Time_ISO}}'); + let includeScope = 2d; + let historicalScope = 8d; + let GetAllLogonsForHost = (v_Host_Name:string, v_Host_NTDomain:string, v_Host_DnsDomain:string, v_Host_AzureID:string, v_Host_OMSAgentID:string){ + let DomainAllowList = dynamic(["NT AUTHORITY", "NT SERVICE", "Font Driver Host", "Window Manager"]); + let AccountAllowList = dynamic(["SYSTEM","NETWORK SERVICE", "LOCAL SERVICE"]); + let AllEvents = SecurityEvent + | where TimeGenerated >= (starttime - historicalScope) + | where EventID in (4624, 4625, 4672) + // parsing for Host to handle variety of conventions coming from data + | extend Host_HostName = case( + Computer has '@', tostring(split(Computer, '@')[0]), + Computer has '\\', tostring(split(Computer, '\\')[1]), + Computer has '.', tostring(split(Computer, '.')[0]), + Computer + ) + | extend Host_NTDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', tostring(split(Computer, '.')[-2]), + Computer + ) + | extend Host_DnsDomain = case( + Computer has '\\', tostring(split(Computer, '\\')[0]), + Computer has '.', strcat_array(array_slice(split(Computer,'.'),-2,-1),'.'), + Computer + ) + | where (Host_HostName =~ v_Host_Name and Host_NTDomain =~ v_Host_NTDomain) + or (Host_HostName =~ v_Host_Name and Host_DnsDomain =~ v_Host_DnsDomain) + or v_Host_AzureID =~ _ResourceId + or v_Host_OMSAgentID == SourceComputerId + | extend RelatedRowSet = 'AllEvents' + | extend HourOfLogin = hourofday(TimeGenerated), DayNumberofWeek = dayofweek(TimeGenerated) + | extend DayofWeek = case( + DayNumberofWeek == "00:00:00", "Sunday", + DayNumberofWeek == "1.00:00:00", "Monday", + DayNumberofWeek == "2.00:00:00", "Tuesday", + DayNumberofWeek == "3.00:00:00", "Wednesday", + DayNumberofWeek == "4.00:00:00", "Thursday", + DayNumberofWeek == "5.00:00:00", "Friday", + DayNumberofWeek == "6.00:00:00", "Saturday","InvalidTimeStamp") + // map the most common ntstatus codes + | extend StatusDesc = case( + Status =~ "0x80090302", "SEC_E_UNSUPPORTED_FUNCTION", + Status =~ "0x80090308", "SEC_E_INVALID_TOKEN", + Status =~ "0x8009030E", "SEC_E_NO_CREDENTIALS", + Status =~ "0xC0000008", "STATUS_INVALID_HANDLE", + Status =~ "0xC0000017", "STATUS_NO_MEMORY", + Status =~ "0xC0000022", "STATUS_ACCESS_DENIED", + Status =~ "0xC0000034", "STATUS_OBJECT_NAME_NOT_FOUND", + Status =~ "0xC000005E", "STATUS_NO_LOGON_SERVERS", + Status =~ "0xC000006A", "STATUS_WRONG_PASSWORD", + Status =~ "0xC000006D", "STATUS_LOGON_FAILURE", + Status =~ "0xC000006E", "STATUS_ACCOUNT_RESTRICTION", + Status =~ "0xC0000073", "STATUS_NONE_MAPPED", + Status =~ "0xC00000FE", "STATUS_NO_SUCH_PACKAGE", + Status =~ "0xC000009A", "STATUS_INSUFFICIENT_RESOURCES", + Status =~ "0xC00000DC", "STATUS_INVALID_SERVER_STATE", + Status =~ "0xC0000106", "STATUS_NAME_TOO_LONG", + Status =~ "0xC000010B", "STATUS_INVALID_LOGON_TYPE", + Status =~ "0xC000015B", "STATUS_LOGON_TYPE_NOT_GRANTED", + Status =~ "0xC000018B", "STATUS_NO_TRUST_SAM_ACCOUNT", + Status =~ "0xC0000224", "STATUS_PASSWORD_MUST_CHANGE", + Status =~ "0xC0000234", "STATUS_ACCOUNT_LOCKED_OUT", + Status =~ "0xC00002EE", "STATUS_UNFINISHED_CONTEXT_DELETED", + EventID == 4624 or EventID == 4672, "Success", + "See - https://docs.microsoft.com/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55" + ) + | extend SubStatusDesc = case( + SubStatus =~ "0x80090325", "SEC_E_UNTRUSTED_ROOT", + SubStatus =~ "0xC0000008", "STATUS_INVALID_HANDLE", + SubStatus =~ "0xC0000022", "STATUS_ACCESS_DENIED", + SubStatus =~ "0xC0000064", "STATUS_NO_SUCH_USER", + SubStatus =~ "0xC000006A", "STATUS_WRONG_PASSWORD", + SubStatus =~ "0xC000006D", "STATUS_LOGON_FAILURE", + SubStatus =~ "0xC000006E", "STATUS_ACCOUNT_RESTRICTION", + SubStatus =~ "0xC000006F", "STATUS_INVALID_LOGON_HOURS", + SubStatus =~ "0xC0000070", "STATUS_INVALID_WORKSTATION", + SubStatus =~ "0xC0000071", "STATUS_PASSWORD_EXPIRED", + SubStatus =~ "0xC0000072", "STATUS_ACCOUNT_DISABLED", + SubStatus =~ "0xC0000073", "STATUS_NONE_MAPPED", + SubStatus =~ "0xC00000DC", "STATUS_INVALID_SERVER_STATE", + SubStatus =~ "0xC0000133", "STATUS_TIME_DIFFERENCE_AT_DC", + SubStatus =~ "0xC000018D", "STATUS_TRUSTED_RELATIONSHIP_FAILURE", + SubStatus =~ "0xC0000193", "STATUS_ACCOUNT_EXPIRED", + SubStatus =~ "0xC0000380", "STATUS_SMARTCARD_WRONG_PIN", + SubStatus =~ "0xC0000381", "STATUS_SMARTCARD_CARD_BLOCKED", + SubStatus =~ "0xC0000382", "STATUS_SMARTCARD_CARD_NOT_AUTHENTICATED", + SubStatus =~ "0xC0000383", "STATUS_SMARTCARD_NO_CARD", + SubStatus =~ "0xC0000384", "STATUS_SMARTCARD_NO_KEY_CONTAINER", + SubStatus =~ "0xC0000385", "STATUS_SMARTCARD_NO_CERTIFICATE", + SubStatus =~ "0xC0000386", "STATUS_SMARTCARD_NO_KEYSET", + SubStatus =~ "0xC0000387", "STATUS_SMARTCARD_IO_ERROR", + SubStatus =~ "0xC0000388", "STATUS_DOWNGRADE_DETECTED", + SubStatus =~ "0xC0000389", "STATUS_SMARTCARD_CERT_REVOKED", + EventID == 4624 or EventID == 4672, "Success", + "See - https://docs.microsoft.com/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55" + ) + | project TimeGenerated, DayofWeek, HourOfLogin, EventID, Activity, IpAddress, WorkstationName, Computer, TargetAccount, TargetUserName, TargetDomainName, ProcessName, SubjectUserName, PrivilegeList, LogonTypeName, StatusDesc, SubStatusDesc, RelatedRowSet, _ResourceId, SourceComputerId + ; + let HostSigninToSystems = materialize(AllEvents + | where EventID == 4624 + | project-away StatusDesc, SubStatusDesc, PrivilegeList + | summarize SigninCount= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName) by EventID, Activity, Computer, TargetAccount, TargetDomainName, TargetUserName , ProcessName , LogonTypeName, _ResourceId, SourceComputerId + | extend RelatedRowSet = 'HostSigninToSystems', DomainAllowList = dynamic(["NT AUTHORITY", "NT SERVICE", "Font Driver Host", "Window Manager"])); + let HostFailedSigninToSystems = materialize(AllEvents + | where EventID == 4625 + | project-away PrivilegeList + | summarize SigninCount= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName) by EventID, Activity, Computer, TargetAccount, TargetDomainName, TargetUserName , ProcessName , LogonTypeName, _ResourceId, SourceComputerId + | extend RelatedRowSet = 'HostFailedSigninToSystems'); + let HostSigninDuringAbnormalHours = materialize(AllEvents + | where TimeGenerated between ((starttime - historicalScope) .. (endtime - includeScope)) + | where EventID in (4624,4625) + | where LogonTypeName in~ ('2 - Interactive','10 - RemoteInteractive') + | summarize max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek) by Computer, _ResourceId, SourceComputerId + | join kind= inner + ( + AllEvents + | where TimeGenerated > endtime - includeScope + | where LogonTypeName in~ ('2 - Interactive','10 - RemoteInteractive') + ) + on Computer + | where HourOfLogin > max_HourOfLogin or HourOfLogin < min_HourOfLogin + | extend historical_DayofWeek = tostring(historical_DayofWeek) + | summarize SigninCount= count(), max(HourOfLogin), min(HourOfLogin), current_DayofWeek =make_set(DayofWeek), StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName) by EventID, Activity, Computer, TargetAccount, TargetDomainName, TargetUserName , ProcessName , LogonTypeName, StatusDesc, SubStatusDesc, historical_DayofWeek, _ResourceId, SourceComputerId + | extend historical_DayofWeek = todynamic(historical_DayofWeek) + | extend RelatedRowSet = 'HostSigninDuringAbnormalHour', DomainAllowList = dynamic(["NT AUTHORITY", "NT SERVICE", "Font Driver Host", "Window Manager"])); + let HostHadPrivilegedLogonSessions = materialize(AllEvents + | where EventID == 4672 + | where PrivilegeList contains 'SeDebugPrivilege' + | project-away StatusDesc, SubStatusDesc + | summarize SigninCount= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName) by EventID, Activity, Computer, TargetAccount, TargetDomainName, TargetUserName, PrivilegeList, _ResourceId, SourceComputerId + | extend RelatedRowSet = 'HostHadPrivilegedLogonSessions', DomainAllowList = dynamic(["NT AUTHORITY", "NT SERVICE", "Font Driver Host", "Window Manager"])); + union isfuzzy=true AllEvents, HostSigninToSystems, HostFailedSigninToSystems, HostSigninDuringAbnormalHours, HostHadPrivilegedLogonSessions + | extend timestamp = StartTime, HostCustomEntity = Computer, AccountCustomEntity = TargetAccount + }; + // change {{Host_HostName}} value below to the HostName you are interested in + GetAllLogonsForHost('{{Host_HostName}}', '{{Host_NTDomain}}', '{{Host_DnsDomain}}', '{{Host_AzureID}}', '{{Host_OMSAgentID}}') +# The queries for the insights. +Insights: + Id: 4ecc2229-5cbf-4b04-a2ab-0842c5e4d1cd + DisplayName: Windows sign-in activity + Description: | + Summary of successful and failed sign-ins along with anamalous sign-in patterns for the specific host. Successful sign-ins currently only include interactive and limited to LogonType 2 and 10. + DefaultTimeRange: + BeforeRange: 12h + AfterRange: 12h + TableQuery: + ColumnsDefinitions: + - Header: "Signin Type" + OutputType: String + - Header: "Signin Count" + OutputType: Number + SupportDeepLink: true + - Header: "User Count" + OutputType: Number + SupportDeepLink: true + - Header: "User(s)" + OutputType: String + QueriesDefinitions: + + # HostSigninToSystems + - Filter: "where RelatedRowSet =~ 'HostSigninToSystems' and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager')" + Summarize: "summarize SigninCount = sum(SigninCount), UserCount = dcount(TargetUserName), Users = make_set(TargetUserName)" + Project: "project Title = 'Successful', SigninCount, UserCount, Users = case(array_length(Users) > 1, 'Many', array_length(Users) == 1, tostring(Users[0]), 'None')" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # HostFailedSigninToSystems + - Filter: "where RelatedRowSet =~ 'HostFailedSigninToSystems'" + Summarize: "summarize SigninCount= sum(SigninCount), UserCount = dcount(TargetUserName), Users = make_set(TargetUserName)" + Project: "project Title = 'Failed', SigninCount, UserCount, Users = case(array_length(Users) > 1, 'Many', array_length(Users) == 1, tostring(Users[0]), 'None')" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # HostSigninDuringAbnormalHours + - Filter: "where RelatedRowSet =~ 'HostSigninDuringAbnormalHour' and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager')" + Summarize: "summarize SigninCount = sum(SigninCount), UserCount = dcount(TargetUserName), Users = make_set(TargetUserName)" + Project: "project Title = 'Abnormal Time', SigninCount, UserCount, Users = case(array_length(Users) > 1, 'Many', array_length(Users) == 1, tostring(Users[0]), 'None')" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # HostHadPrivilegedLogonSessions + - Filter: "where RelatedRowSet =~ 'HostHadPrivilegedLogonSessions' " + Summarize: "summarize SigninCount = sum(SigninCount), UserCount = dcount(TargetUserName), Users = make_set(TargetUserName)" + Project: "project Title = 'Privileged', SigninCount, UserCount, Users = case(array_length(Users) > 1, 'Many', array_length(Users) == 1, tostring(Users[0]), 'None')" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # MostFrequent + - Filter: "where RelatedRowSet =~ 'AllEvents' | where EventID == 4624 and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager')" + Summarize: "summarize StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SigninCount = count() by TargetDomainName, TargetUserName, EventID | top 1 by SigninCount desc" + Project: "project Title = 'Most Frequent', SigninCount, UserCount = 1, Users = TargetUserName" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + # LeastFrequent + - Filter: "where RelatedRowSet =~ 'AllEvents' | where EventID == 4624 and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager')" + Summarize: "summarize StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SigninCount = count() by TargetDomainName, TargetUserName, EventID | top 1 by SigninCount asc" + Project: "project Title = 'Least Frequent', SigninCount, UserCount = 1, Users = TargetUserName" + LinkColumnsDefinitions: + - ProjectedName: SigninCount + Query: "{{BaseQuery}} | {{RowFilter}}" + - ProjectedName: UserCount + Query: "{{BaseQuery}} | {{RowFilter}}" + + ChartQuery: + Title: "Sign-ins over time" + DataSets: + - Query: "where RelatedRowSet =~ 'AllEvents' and EventID == 4624 and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager') | summarize Count=count() by Time = bin(TimeGenerated, 1h) | extend Legend = 'Success'" + XColumnName: "Time" + YColumnName: "Count" + LegendColumnName: "Legend" + - Query: "where RelatedRowSet =~ 'AllEvents' and EventID == 4625 | summarize Count=count() by Time = bin(TimeGenerated, 1h) | extend Legend = 'Failed'" + XColumnName: "Time" + YColumnName: "Count" + LegendColumnName: "Legend" + Type: LineChart + + AdditionalQuery: + Text: "See all windows sign-ins" + Query: "where RelatedRowSet =~ 'AllEvents' | extend SubjectUserName = columnifexists('SubjectUserName', 'EventDoesNotContain') | summarize SigninCount= count(), max(HourOfLogin), min(HourOfLogin), historical_DayofWeek=make_set(DayofWeek), StartTime=min(TimeGenerated), EndTime = max(TimeGenerated), SourceIP = make_set(IpAddress), SourceHost = make_set(WorkstationName), SubjectUserName = make_set(SubjectUserName), UserCount = dcount(TargetUserName), Users = make_set(TargetUserName) by Activity, TargetDomainName, TargetUserName, ProcessName, LogonTypeName, timestamp, HostCustomEntity, AccountCustomEntity" + +Activities: + EnabledByDefault: true + Items: + - Id: cfba48ac-49dd-4d8a-8a65-70eaf4aafb61 + Description: Privileged logon by account + Title: "Privileged logon by account on this host" + Content: "On '{{Computer}}' the user '{{TargetUserName}}' logged on at least once with the SeDebugPrivilege" + QueryDefinitions: + Filter: where RelatedRowSet =~ 'HostHadPrivilegedLogonSessions' + SummarizeBy: TargetUserName + - Id: 8d639acf-f55f-40cd-8ada-bf81b277bb73 + Description: Logon outside normal hours + Title: "An account has logged on outside of their normal hours on this host" + Content: "On '{{Computer}}' the user '{{TargetUserName}}' logged on at least once outside of their normal logon hours" + QueryDefinitions: + Filter: where RelatedRowSet =~ 'HostSigninDuringAbnormalHour' and TargetDomainName !in ('NT AUTHORITY', 'NT SERVICE', 'Font Driver Host', 'Window Manager') + SummarizeBy: TargetUserName \ No newline at end of file diff --git a/Insights/readme.md b/Insights/readme.md new file mode 100644 index 0000000000..8c8ada38b5 --- /dev/null +++ b/Insights/readme.md @@ -0,0 +1 @@ +Insights feature preview \ No newline at end of file