Home/Detection rules/Splunk ESCU
Tool

Splunk ESCU

633 vendor-native detections · ready to paste into your SIEM · cross-linked to ATT&CK

Detections

33 shown of 633
Microsoft Sentinel KQL T1566 ↗
Star Blizzard C2 Domains August 2022
'Identifies a match across various data feeds for domains related to an actor tracked by Microsoft as Star Blizzard.'
Show query
let iocs = externaldata(DateAdded:string,IoC:string,Type:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/SEABORGIUMIOC.csv"] with (format="csv", ignoreFirstRecord=True);
let DomainNames = (iocs | where Type =~ "domainname"| project IoC);
(union isfuzzy=true
(CommonSecurityLog
| parse Message with * '(' DNSName ')' *
| where DNSName in~ (DomainNames)
| extend Account = SourceUserID, Computer = DeviceName, IPAddress =  DestinationIP
),
(_Im_Dns (domain_has_any=DomainNames)
| extend DNSName = DnsQuery
| extend IPAddress =  SrcIpAddr, Computer = Dvc
),
(_Im_WebSession (url_has_any=DomainNames)
| extend DNSName = tostring(parse_url(Url)["Host"])
| extend IPAddress =  SrcIpAddr, Computer = Dvc
),
(VMConnection
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| where DNSName  in~ (DomainNames)
| extend IPAddress = RemoteIp
),
(DeviceNetworkEvents
| where RemoteUrl  has_any (DomainNames)
| extend IPAddress = RemoteIP
| extend Computer = DeviceName
),
(EmailUrlInfo
| where Url has_any (DomainNames)
| join (EmailEvents
| where EmailDirection == "Inbound" ) on NetworkMessageId
| extend IPAddress = SenderIPv4
| extend Account = RecipientEmailAddress
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where isnotempty(DestinationHost)
| where DestinationHost has_any (DomainNames)
| extend DNSName = DestinationHost
| extend IPAddress = SourceHost
)
)
| extend AccountName = tostring(split(Account, "@")[0]), AccountUPNSuffix = tostring(split(Account, "@")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "@")[0]), AccountUPNSuffix = tostring(split(Account, "@")[1])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
Microsoft Sentinel KQL T1078.004 ↗
Suspicious Login from deleted guest account
' This query will detect logins from guest account which was recently deleted. For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion'
Show query
let query_frequency = 1h;
let query_period = 1d;
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
| mv-expand TargetResource = TargetResources
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
| extend
    Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
    InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
    Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(query_period)
    | where ResultType == 0
    | summarize take_any(*) by UserPrincipalName
    | extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
    | project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
    ) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
| where SigninLogs_TimeGenerated > Delete_TimeGenerated
| project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
| extend
    AccountName = tostring(split(UserPrincipalName, "@")[0]),
    AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel KQL T1078.004 ↗
Suspicious Sign In by Entra ID Connect Sync Account
'This query looks for sign ins by the Microsoft Entra ID Connect Sync account to Azure where properties about the logon are anomalous. This query uses Microsoft Sentinel's UEBA features to detect these suspicious properties. A threat actor may attempt to steal the Sync account credentials and use them to access Azure resources. This alert should be reviewed to ensure that the log in came was from a legitimate source.'
Show query
BehaviorAnalytics
// User modification is expected from this account so focus on logons
| where ActivityType =~ "LogOn"
| where UserName startswith "Sync_" and UsersInsights.AccountDisplayName =~ "On-Premises Directory Synchronization Service Account"
// Filter out this expected activity
| where ActivityInsights.App !~ "Microsoft Azure Active Directory Connect"
| where InvestigationPriority > 0
| extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]
Microsoft Sentinel KQL T1078 ↗
Suspicious VM Instance Creation Activity Detected
'This detection identifies high-severity alerts across various Microsoft security products, including Microsoft Defender XDR and Microsoft Entra ID, and correlates them with instances of Google Cloud VM creation. It focuses on instances where VMs were created within a short timeframe of high-severity alerts, potentially indicating suspicious activity.'
Show query
// Filter alerts from specific Microsoft security products with medium and high severity
SecurityAlert 
| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security", "Azure Active Directory Identity Protection", "Microsoft Defender ATP")
| where AlertSeverity has_any ("Medium", "High")
// Parse JSON entities and extend AlertTimeGenerated
| extend Entities = parse_json(Entities), AlertTimeGenerated=TimeGenerated
// Extract and process IP entities
| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'ip' 
    | extend EntityIp = tostring(Entity.Address) 
    ) 
// Extract and process account entities
| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'account' 
    | extend AccountObjectId = tostring(Entity.AadUserId)
    )
// Filter out records with empty EntityIp
| where isnotempty(EntityIp)
// Summarize data and create sets of entities and system alert IDs
| summarize Entitys=make_set(Entity), SystemAlertIds=make_set(SystemAlertId)
    by 
    AlertName,
    ProductName,
    AlertSeverity,
    EntityIp,
    Tactics,
    Techniques,
    ProviderName,
    AlertTime= bin(AlertTimeGenerated, 1d),
    AccountObjectId
// Join with GCPAuditLogs for VM instance creation
| join kind=inner (
    GCPAuditLogs
    | where ServiceName == "compute.googleapis.com" and MethodName endswith "instances.insert"
    | extend
        GCPUserUPN= tostring(parse_json(AuthenticationInfo).principalEmail),
        GCPUserIp = tostring(parse_json(RequestMetadata).callerIp),
        GCPUserUA= tostring(parse_json(RequestMetadata).callerSuppliedUserAgent),
        VMStatus =  tostring(parse_json(Response).status),
        VMOperation=tostring(parse_json(Response).operationType),
        VMName= tostring(parse_json(Request).name),
        VMType = tostring(split(parse_json(Request).machineType, "/")[-1])
    | where GCPUserUPN !has "gserviceaccount.com"
    | where VMOperation == "insert" and isnotempty(GCPUserIp) and GCPUserIp != "private"
    | project
        GCPOperationTime=TimeGenerated,
        VMName,
        VMStatus,
        MethodName,
        GCPUserUPN,
        ProjectId,
        GCPUserIp,
        GCPUserUA,
        VMOperation,
        VMType
    )
    on $left.EntityIp == $right.GCPUserIp 
// Join with IdentityInfo to enrich user identity details
| join kind=inner (IdentityInfo 
    | distinct AccountObjectId, AccountUPN, JobTitle
    )
    on AccountObjectId 
// Calculate the time difference between the alert and VM creation for further analysis
| extend TimeDiff= datetime_diff('day', AlertTime, GCPOperationTime),Name = split(GCPUserUPN, "@")[0], UPNSuffix = split(GCPUserUPN, "@")[1]
Microsoft Sentinel KQL T1598 ↗
Suspicious link sharing pattern
'Alerts in links that have been shared across multiple Zoom chat channels by the same user in a short space if time. Adjust the threshold figure to change the number of channels a message needs to be posted in before an alert is raised.'
Show query
let threshold = 3;
ZoomLogs
| where Event =~ "chat_message.sent"
| extend Channel = tostring(parse_json(ChatEvents).Channel)
| extend Message = tostring(parse_json(ChatEvents).Message)
| where Message matches regex "http(s?):\\/\\/"
| summarize Channels = makeset(Channel), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Message, User, UserId
| extend ChannelCount = arraylength(Channels)
| where ChannelCount > threshold
| extend AccountName = tostring(split(User, "@")[0]), AccountUPNSuffix = tostring(split(User, "@")[1])
Microsoft Sentinel KQL T1078.004 ↗
Suspicious linking of existing user to external User
' This query will detect when an attempt is made to update an existing user and link it to an guest or external identity. These activities are unusual and such linking of external identities should be investigated. In some cases you may see internal Entra ID sync accounts (Sync_) do this which may be benign'
Show query
AuditLogs
| where OperationName=~ "Update user" 
| where Result =~ "success" 
| mv-expand TargetResources 
| mv-expand TargetResources.modifiedProperties 
| extend displayName = tostring(TargetResources_modifiedProperties.displayName), 
TargetUPN_oldValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))[0]), 
TargetUPN_newValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue))[0])
| where displayName == "UserPrincipalName" and TargetUPN_oldValue !has "#EXT" and TargetUPN_newValue has "#EXT"
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| summarize arg_max(TimeGenerated, *) by CorrelationId
| project-reorder TimeGenerated, InitiatedBy, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUPN_oldValue, TargetUPN_newValue
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(TargetUPN_oldValue, "@")[0]), TargetUPNSuffix = tostring(split(TargetUPN_oldValue, "@")[1])
Microsoft Sentinel KQL T1078.004 ↗
Suspicious modification of Global Administrator user properties
'This query will detect if user properties of Global Administrator are updated by an existing user. Usually only user administrator or other global administrator can update such properties. Investigate if such user change is an attempt to elevate an existing low privileged identity or rogue administrator activity'
Show query
let query_frequency = 1h;
let query_period = 14d;
IdentityInfo
| where TimeGenerated > ago(query_period)
| where set_has_element(AssignedRoles, "Global Administrator")
| distinct AccountUPN, AccountObjectId
| join kind=inner (
    AuditLogs
    | where TimeGenerated > ago(query_frequency)
    | where OperationName=~ "Update user" and Result =~ "success"
    // | where isnotempty(InitiatedBy["user"])
    | mv-expand TargetResource = TargetResources
    | where TargetResource["type"] == "User"
    | extend AccountObjectId = tostring(TargetResource["id"])
    | where tostring(TargetResource["modifiedProperties"]) != "[]"
    | mv-apply modifiedProperty = TargetResource["modifiedProperties"] on (
        summarize modifiedProperties = make_bag(
            bag_pack(tostring(modifiedProperty["displayName"]),
                bag_pack("oldValue", trim(@'[\"\s]+', tostring(modifiedProperty["oldValue"])),
                    "newValue", trim(@'[\"\s]+', tostring(modifiedProperty["newValue"])))))
    )
    | where not(tostring(modifiedProperties["Included Updated Properties"]["newValue"]) in ("LastDirSyncTime", ""))
    | where not(tostring(modifiedProperties["Included Updated Properties"]["newValue"]) == "StrongAuthenticationPhoneAppDetail" and isnotempty(modifiedProperties["StrongAuthenticationPhoneAppDetail"]) and tostring(array_sort_asc(extract_all(@'\"Id\"\:\"([^\"]+)\"', tostring(modifiedProperties["StrongAuthenticationPhoneAppDetail"]["newValue"])))) == tostring(array_sort_asc(extract_all(@'\"Id\"\:\"([^\"]+)\"', tostring(modifiedProperties["StrongAuthenticationPhoneAppDetail"]["oldValue"])))))
    | extend
        Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
        InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
        IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
) on AccountObjectId
| project TimeGenerated, Category, Identity, Initiator, IPAddress, OperationName, Result, AccountUPN, InitiatedBy, AdditionalDetails, TargetResources, AccountObjectId, InitiatorId, CorrelationId
| extend
    InitiatorName = tostring(split(Initiator, "@")[0]),
    InitiatorUPNSuffix = tostring(split(Initiator, "@")[1]),
    AccountName = tostring(split(AccountUPN, "@")[0]),
    AccountUPNSuffix = tostring(split(AccountUPN, "@")[1])
Microsoft Sentinel KQL
SysLog-DetectAnomaliesInEvents
Show query
//Detect potential anomalous increase in syslog volume, adjust time frames to suit
let Computers=Syslog_CL
    | where TimeGenerated >= ago(4d)
    | summarize EventCount=count() by Computer, bin(TimeGenerated, 15m)
    | where EventCount >= 1000
    | order by TimeGenerated
    | summarize EventCount=make_list(EventCount), TimeGenerated=make_list(TimeGenerated) by Computer
    | extend outliers=series_decompose_anomalies(EventCount, 2)
    | mv-expand TimeGenerated, EventCount, outliers
    | where outliers == 1
    | distinct Computer
;
Syslog_CL
| where TimeGenerated >= ago(4d)
| where Computer in (Computers)
| summarize EventCount=count() by Computer, bin(TimeGenerated, 15m)
| render timechart
Microsoft Sentinel KQL T1030 ↗
Time series anomaly detection for total volume of traffic
'Identifies anamalous spikes in network traffic logs as compared to baseline or normal historical patterns. The query leverages a KQL built-in anomaly detection algorithm to find large deviations from baseline patterns. Sudden increases in network traffic volume may be an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP addres
Show query
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let percentotalthreshold = 50;
let TimeSeriesData = CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| project TimeGenerated,SourceIP, DestinationIP, DeviceVendor
| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor;
// Filtering specific records associated with spikes as outliers
let TimeSeriesAlerts=materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, scorethreshold, -1, 'linefit')
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend score = round(score,2), AnomalyHour = TimeGenerated
| project DeviceVendor,AnomalyHour, TimeGenerated, Total, baseline, anomalies, score);
let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
// Join anomalies with Base Data to popalate associated records for investigation - Results sorted by score in descending order
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join (
    CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| summarize HourlyCount = count(), TimeGeneratedMax = arg_max(TimeGenerated, *), DestinationIPlist = make_set(DestinationIP, 100), DestinationPortlist = make_set(DestinationPort, 100) by DeviceVendor, SourceIP, TimeGeneratedHour= bin(TimeGenerated, 1h)
| extend AnomalyHour = TimeGeneratedHour
) on AnomalyHour, DeviceVendor
| extend PercentTotal = round((HourlyCount / Total) * 100, 3)
| where PercentTotal > percentotalthreshold
| project DeviceVendor , AnomalyHour, TimeGeneratedMax, SourceIP, DestinationIPlist, DestinationPortlist, HourlyCount, PercentTotal, Total, baseline, score, anomalies
| summarize HourlyCount=sum(HourlyCount), StartTimeUtc=min(TimeGeneratedMax), EndTimeUtc=max(TimeGeneratedMax), SourceIPlist = make_set(SourceIP, 100), SourceIPMax= arg_max(SourceIP, *), DestinationIPlist = make_set(DestinationIPlist, 100), DestinationPortlist = make_set(DestinationPortlist, 100) by DeviceVendor , AnomalyHour, Total, baseline, score, anomalies
| project DeviceVendor , AnomalyHour, EndTimeUtc, SourceIPMax ,SourceIPlist, DestinationIPlist, DestinationPortlist, HourlyCount, Total, baseline, score, anomalies
Microsoft Sentinel KQL T1030 ↗
Time series anomaly for data size transferred to public internet
'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern. A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic
Show query
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let bytessentperhourthreshold = 10;
let TimeSeriesData = (union isfuzzy=true
(
VMConnection
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false
| extend DeviceVendor = "VMConnection"
| project TimeGenerated, BytesSent, DeviceVendor
| make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
),
(
CommonSecurityLog
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where ipv4_is_private(DestinationIP) == false
| project TimeGenerated, SentBytes, DeviceVendor
| make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
)
);
//Filter anomolies against TimeSeriesData
let TimeSeriesAlerts = materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(TotalBytesSent, scorethreshold, -1, 'linefit')
| mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend AnomalyHour = TimeGenerated
| extend TotalBytesSentinMBperHour = round(((TotalBytesSent / 1024)/1024),2), baselinebytessentperHour = round(((baseline / 1024)/1024),2), score = round(score,2)
| project DeviceVendor, AnomalyHour, TimeGenerated, TotalBytesSentinMBperHour, baselinebytessentperHour, anomalies, score);
let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
//Union of all BaseLogs aggregated per hour
let BaseLogs = (union isfuzzy=true
(
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| where ipv4_is_private(DestinationIP) == false
| extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
| summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
| where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
),
(
VMConnection
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false | extend DeviceVendor = "VMConnection"
| extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
| summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
| where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
)
);
// Join against base logs to retrive records associated with the hour of anomoly
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join (
    BaseLogs | extend AnomalyHour = TimeGeneratedHour
) on DeviceVendor, AnomalyHour | sort by score desc
| project DeviceVendor, AnomalyHour,TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
| summarize EventCount = count(), StartTimeUtc= min(TimeGeneratedMax), EndTimeUtc= max(TimeGeneratedMax), SourceIPMax= arg_max(SourceIP,*), TotalBytesSentinMB = sum(TotalSentBytesinMB), TotalBytesReceivedinMB = sum(TotalReceivedBytesinMB), SourceIPList = make_set(SourceIP, 100), DestinationIPList = make_set(DestinationIPList, 100) by AnomalyHour,TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
| project DeviceVendor, AnomalyHour, StartTimeUtc, EndTimeUtc, SourceIPMax, SourceIPList, DestinationIPList, DestinationPortList, TotalBytesSentinMB, TotalBytesReceivedinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies, EventCount
Microsoft Sentinel KQL T1528 ↗
Trust Monitor Event
'This query identifies when a new trust monitor event is detected.'
Show query
let timeframe = ago(5m);
DuoSecurityTrustMonitor_CL
| where TimeGenerated >= timeframe
| extend AccountName = tostring(split(surfaced_auth_user_name_s, "@")[0]), AccountUPNSuffix = tostring(split(surfaced_auth_user_name_s, "@")[1])
Microsoft Sentinel KQL T1078.004 ↗
URL Added to Application from Unknown Domain
'Detects a URL being added to an application where the domain is not one that is associated with the tenant. The query uses domains seen in sign in logs to determine if the domain is associated with the tenant. Applications associated with URLs not controlled by the organization can pose a security risk. Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes'
Show query
let domains =
  SigninLogs
  | where ResultType == 0
  | extend domain = split(UserPrincipalName, "@")[1]
  | extend domain = tostring(split(UserPrincipalName, "@")[1])
  | summarize by tolower(tostring(domain));
  AuditLogs
  | where Category =~ "ApplicationManagement"
  | where Result =~ "success"
  | where OperationName =~ 'Update Application'
  | mv-expand TargetResources
  | mv-expand TargetResources.modifiedProperties
  | where TargetResources_modifiedProperties.displayName =~ "AppAddress"
  | extend Key = tostring(TargetResources_modifiedProperties.displayName)
  | extend NewValue = TargetResources_modifiedProperties.newValue
  | extend OldValue = TargetResources_modifiedProperties.oldValue
  | where isnotempty(Key) and isnotempty(NewValue)
  | project-reorder Key, NewValue, OldValue
  | extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
  | extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
  | extend AddedUrls = set_difference(NewUrls, OldUrls)
  | where array_length(AddedUrls) > 0
  | extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
  | extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
  | extend AppDisplayName = tostring(TargetResources.displayName)
  | where isnotempty(AddedUrls)
  | mv-expand AddedUrls
  | extend AddedUrls = trim(@'"', tostring(AddedUrls))
  | extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
  | where isnotempty(Domain)
  | extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])
  | where Domain !in (domains)
  | project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Microsoft Sentinel KQL
Unusual Anomaly
'Anomaly Rules generate events in the Anomalies table. This scheduled rule tries to detect Anomalies that are not usual, they could be a type of Anomaly that has recently been activated, or an infrequent type. The detected Anomaly should be reviewed, if it is relevant enough, eventually a separate scheduled Analytics Rule could be created specifically for that Anomaly Type, so an alert and/or incident is generated everytime that type of Anomaly happens.'
Show query
// You can leave out Anomalies that are already monitored through other Analytics Rules
//let _MonitoredRules = dynamic(["TestAlertName"]);
let query_frequency = 1h;
let query_lookback = 3d;
Anomalies
| where TimeGenerated > ago(query_frequency)
//| where not(RuleName has_any (_MonitoredRules))
| join kind = leftanti (
    Anomalies
    | where TimeGenerated between (ago(query_frequency + query_lookback)..ago(query_frequency))
    | distinct RuleName
) on RuleName
| extend Name = tostring(split(UserPrincipalName, "@")[0]), UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel KQL T1136 ↗
Unusual identity creation using exchange powershell
' The query below identifies creation of unusual identity by the Europium actor to mimic Microsoft Exchange Health Manager Service account using Exchange PowerShell commands Reference: https://www.microsoft.com/security/blog/2022/09/08/microsoft-investigates-iranian-attacks-against-the-albanian-government/'
Show query
(union isfuzzy=true
(SecurityEvent
| where EventID==4688
| where CommandLine has_any ("New-Mailbox","Update-RoleGroupMember") and CommandLine has "HealthMailbox55x2yq"
| project TimeGenerated, DeviceName = Computer, AccountName = SubjectUserName, AccountDomain = SubjectDomainName, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
| extend InitiatingProcessAccount = strcat(AccountDomain, "\\", AccountName)
),
(DeviceProcessEvents
| where ProcessCommandLine has_any ("New-Mailbox","Update-RoleGroupMember") and ProcessCommandLine has "HealthMailbox55x2yq"
| extend timestamp = TimeGenerated, AccountDomain = InitiatingProcessAccountDomain, AccountName = InitiatingProcessAccountName
| extend InitiatingProcessAccount = strcat(AccountDomain, "\\", AccountName)
)
)
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
Microsoft Sentinel KQL T1136.003 ↗
User Account Created Using Incorrect Naming Format
'This query looks for accounts being created where the name does not match a defined pattern. Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts. Created accounts should be investigated to ensure they were legitimated created. The user_regex field in the query needs to be populated with the expected pattern for the environment before deployment. R
Show query
// Add the environments expected username format regex below before deploying
let user_regex = "";
AuditLogs
| where OperationName =~ "Add user"
| where Result =~ "success"
| extend userAgent = tostring(AdditionalDetails[0].value)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AddedUser = tostring(TargetResources[0].userPrincipalName)
| where AddedUser matches regex user_regex
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(AddedUser, "@")[0]), TargetAccountUPNSuffix = tostring(split(AddedUser, "@")[1])
Microsoft Sentinel KQL T1098 ↗
User State changed from Guest to Member
'Detects when a guest account in a tenant is converted to a member of the tenant. Monitoring guest accounts and the access they are provided is important to detect potential account abuse. Accounts converted to members should be investigated to ensure the activity was legitimate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
Show query
AuditLogs
  | where OperationName =~ "Update user"
  | where Result =~ "success"
  | mv-expand TargetResources
  | mv-expand TargetResources.modifiedProperties
  | where TargetResources_modifiedProperties.displayName =~ "TargetId.UserType"
  | extend UpdatingAppName = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
  | extend UpdatingServicePrincipalId = tostring(parse_json(tostring(InitiatedBy.app)).servicePrincipalId)
  | extend UpdatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend UpdatingUserAadUserId = tostring(parse_json(tostring(InitiatedBy.user)).id)
  | extend UpdatingUserIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
  | extend UpdatingUser = iif(isnotempty(UpdatingServicePrincipalId), UpdatingServicePrincipalId, UpdatingUserPrincipalName)
  | extend UpdatedUserPrincipalName = tostring(TargetResources.userPrincipalName)
  | project-reorder TimeGenerated, UpdatedUserPrincipalName, UpdatingUser
  | where parse_json(tostring(TargetResources_modifiedProperties.newValue)) =~ "\"Member\"" and parse_json(tostring(TargetResources_modifiedProperties.oldValue)) =~ "\"Guest\""
  | extend InitiatingAccountName = tostring(split(UpdatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(UpdatingUserPrincipalName, "@")[1])
  | extend TargetAccountName = tostring(split(UpdatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(UpdatedUserPrincipalName, "@")[1])
Microsoft Sentinel KQL T1098 ↗
User account added to built in domain local or global group
'Identifies when a user account has been added to a privileged built in domain local group or global group such as the Enterprise Admins, Cert Publishers or DnsAdmins. Be sure to verify this is an expected addition.'
Show query
// For AD SID mappings - https://docs.microsoft.com/windows/security/identity-protection/access-control/active-directory-security-groups
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$";
union isfuzzy=true
(
SecurityEvent
// 4728 - A member was added to a security-enabled global group
// 4732 - A member was added to a security-enabled local group
// 4756 - A member was added to a security-enabled universal group
| where EventID in (4728, 4732, 4756)
| where AccountType == "User"
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
// Exclude Remote Desktop Users group: S-1-5-32-555
| where TargetSid !in ("S-1-5-32-555")
| extend SimpleMemberName = iff(MemberName == "-", MemberName, substring(MemberName, 3, indexof_regex(MemberName, @",OU|,CN") - 3))
| project TimeGenerated, EventID, Activity, Computer, SimpleMemberName, MemberName, MemberSid, TargetAccount, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, 
SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid
),
(
WindowsEvent
// 4728 - A member was added to a security-enabled global group
// 4732 - A member was added to a security-enabled local group
// 4756 - A member was added to a security-enabled universal group
| where EventID in (4728, 4732, 4756) and not(EventData has "S-1-5-32-555")
| extend SubjectUserSid = tostring(EventData.SubjectUserSid)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountType=case(Account endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
| extend MemberName = tostring(EventData.MemberName)
| where AccountType == "User"
| extend TargetSid = tostring(EventData.TargetSid)
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
// Exclude Remote Desktop Users group: S-1-5-32-555
| where TargetSid !in ("S-1-5-32-555")
| extend SimpleMemberName = iff(MemberName == "-", MemberName, substring(MemberName, 3, indexof_regex(MemberName, @",OU|,CN") - 3))
| extend MemberSid = tostring(EventData.MemberSid)
| extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName), 
TargetAccount = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend UserPrincipalName = tostring(EventData.UserPrincipalName)
| extend SubjectUserName = tostring(EventData.SubjectUserName), SubjectDomainName = tostring(EventData.SubjectDomainName), 
SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| project TimeGenerated, EventID, Computer, SimpleMemberName, MemberName, MemberSid, TargetAccount, TargetUserName, TargetDomainName, TargetSid, UserPrincipalName, 
SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid
)
| extend GroupAddedMemberTo = TargetAccount, AddedByAccount = SubjectAccount, AddedByAccountName = SubjectUserName, AddedByAccountDomainName = SubjectDomainName, 
AddedByAccountSid = SubjectUserSid, AddedMemberName = SimpleMemberName, AddedMemberSid = MemberSid
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Microsoft Sentinel KQL T1098 ↗
User account created and deleted within 10 mins
'Identifies when a user account is created and then deleted within 10 minutes. This can be an indication of compromise and an adversary attempting to hide in the noise.'
Show query
let timeframe = 1d;
let spanoftime = 10m;
let threshold = 0;
(union isfuzzy=true
    (SecurityEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    // A user account was created
    | where EventID == 4720
    | where AccountType =~ "User"
    | project creationTime = TimeGenerated, CreateEventID = EventID, CreateActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToCreate = SubjectAccount, CreatedBySubjectUserName = SubjectUserName, CreatedBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToCreate = SubjectUserSid, UserPrincipalName
    ),
    (
    WindowsEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    // A user account was created
    | where EventID == 4720
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType =~ "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | extend Activity = "4720 - A user account was created."
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | project creationTime = TimeGenerated, CreateEventID = EventID, CreateActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToCreate = SubjectAccount, CreatedBySubjectUserName = SubjectUserName, CreatedBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToCreate = SubjectUserSid, UserPrincipalName
    )
  )
| join kind = inner 
(
  (union isfuzzy=true
    (SecurityEvent
    | where TimeGenerated > ago(timeframe)
    // A user account was deleted
    | where EventID == 4726
    | where AccountType == "User"
    | project deletionTime = TimeGenerated, DeleteEventID = EventID, DeleteActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDelete = SubjectAccount, DeletedBySubjectUserName = SubjectUserName, DeletedBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDelete = SubjectUserSid, UserPrincipalName
    ),
    (WindowsEvent
    | where TimeGenerated > ago(timeframe)
    // A user account was deleted
    | where EventID == 4726
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend AccountType=case(SubjectAccount endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType == "User"
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | extend Activity = "4726 - A user account was deleted."
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | project deletionTime = TimeGenerated, DeleteEventID = EventID, DeleteActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDelete = SubjectAccount, DeletedBySubjectUserName = SubjectUserName, DeletedBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDelete = SubjectUserSid, UserPrincipalName
    )
  )
) on Computer, TargetAccount
| where deletionTime - creationTime < spanoftime
| extend TimeDelta = deletionTime - creationTime
| where tolong(TimeDelta) >= threshold
| project TimeDelta, creationTime, CreateEventID, CreateActivity, Computer, TargetAccount, TargetSid, UserPrincipalName, AccountUsedToCreate, SIDofAccountUsedToCreate,
deletionTime, DeleteEventID, DeleteActivity, AccountUsedToDelete, SIDofAccountUsedToDelete, TargetUserName, TargetDomainName, 
CreatedBySubjectUserName, CreatedBySubjectDomainName, DeletedBySubjectUserName, DeletedBySubjectDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Microsoft Sentinel KQL T1136.003 ↗
User account created without expected attributes defined
'This query looks for accounts being created that do not have attributes populated that are commonly populated in the tenant. Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts. Created accounts should be investigated to ensure they were legitimated created. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user
Show query
let threshold = 10;
let default_ad_attributes = dynamic(["LastDirSyncTime", "StsRefreshTokensValidFrom", "Included Updated Properties", "AccountEnabled", "Action Client Name", "SourceAnchor"]);
let addUsers = AuditLogs
| where OperationName =~ "Add user"
| where Result =~ "success"
| extend AccountProperties = TargetResources[0].modifiedProperties
| mv-expand AccountProperties
;
addUsers
| evaluate bag_unpack(AccountProperties) : (displayName:string, oldValue: string, newValue: string , TenantId : string, SourceSystem : string, TimeGenerated : datetime, ResourceId : string, OperationName : string, OperationVersion : string, Category : string, ResultType : string, ResultSignature : string, ResultDescription : string, DurationMs : long, CorrelationId : string, Resource : string, ResourceGroup : string, ResourceProvider : string, Identity : string, Level : string, Location : string, AdditionalDetails : dynamic, Id : string, InitiatedBy : dynamic, LoggedByService : string, Result : string, ResultReason : string, TargetResources : dynamic, AADTenantId : string, ActivityDisplayName : string, ActivityDateTime : datetime, AADOperationType : string, Type : string)
| extend displayName = column_ifexists("displayName", "Unknown Value")
| summarize count() by displayName, TenantId
| where displayName !in (default_ad_attributes)
| top threshold by count_ desc
| summarize make_set(displayName) by TenantId
| join kind=inner (
addUsers
| extend CreatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatingAadUserId = tostring(InitiatedBy.user.id)
| extend CreatingUserIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
| extend PropName = tostring(AccountProperties.displayName)) 
on TenantId
| summarize makeset(PropName) by TimeGenerated, CorrelationId, CreatedUserPrincipalName, CreatingUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, tostring(set_displayName)
| extend missing_props = set_difference(todynamic(set_displayName), set_PropName)
| where array_length(missing_props) > 0
| join kind=innerunique (
AuditLogs
| where Result =~ "success"
| where OperationName =~ "Add user"
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)) 
on CorrelationId, CreatedUserPrincipalName
| extend ExpectedProperties = set_displayName
| project-away set_displayName, set_PropName
| extend InitiatingAccountName = tostring(split(CreatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(CreatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(CreatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(CreatedUserPrincipalName, "@")[1])
Microsoft Sentinel KQL T1098 ↗
User account enabled and disabled within 10 mins
'Identifies when a user account is enabled and then disabled within 10 minutes. This can be an indication of compromise and an adversary attempting to hide in the noise.'
Show query
let timeframe = 1d;
let spanoftime = 10m;
let threshold = 0;
  (union isfuzzy=true
    (SecurityEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    // A user account was enabled
    | where EventID == 4722
    | where AccountType =~ "User"
    | where TargetAccount !endswith "$"
    | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName
    ),
    (
    WindowsEvent
    | where TimeGenerated > ago(timeframe+spanoftime)
    // A user account was enabled
    | where EventID == 4722
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType =~ "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | where TargetAccount !endswith "$"
    | extend Activity="4722 - A user account was enabled."
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | project EnableTime = TimeGenerated, EnableEventID = EventID, EnableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToEnable = SubjectAccount, EnabledBySubjectUserName = SubjectUserName, EnabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToEnable = SubjectUserSid, UserPrincipalName
    )
  )
| join kind= inner (
  (union isfuzzy=true
    (SecurityEvent
    | where TimeGenerated > ago(timeframe)
    // A user account was disabled
    | where EventID == 4725
    | where AccountType =~ "User"
    | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
    ),
    (WindowsEvent
    | where TimeGenerated > ago(timeframe)
    // A user account was disabled
    | where EventID == 4725
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType =~ "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend TargetSid = tostring(EventData.TargetSid)
    | extend UserPrincipalName = tostring(EventData.UserPrincipalName)
    | extend Activity = "4725 - A user account was disabled."
    | project DisableTime = TimeGenerated, DisableEventID = EventID, DisableActivity = Activity, Computer = toupper(Computer), 
    TargetAccount = tolower(TargetAccount), TargetUserName, TargetDomainName, TargetSid, 
    AccountUsedToDisable = SubjectAccount, DisabledBySubjectUserName = SubjectUserName, DisabledBySubjectDomainName = SubjectDomainName, SIDofAccountUsedToDisable = SubjectUserSid, UserPrincipalName
    )
  )
) on Computer, TargetAccount
| where DisableTime - EnableTime < spanoftime
| extend TimeDelta = DisableTime - EnableTime
| where tolong(TimeDelta) >= threshold
| project TimeDelta, EnableTime, EnableEventID, EnableActivity, Computer, TargetAccount, TargetSid, TargetUserName, TargetDomainName, UserPrincipalName, 
AccountUsedToEnable, SIDofAccountUsedToEnable, DisableTime, DisableEventID, DisableActivity, AccountUsedToDisable, SIDofAccountUsedToDisable, 
EnabledBySubjectUserName, EnabledBySubjectDomainName, DisabledBySubjectUserName, DisabledBySubjectDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Microsoft Sentinel KQL T1078 ↗
User joining Zoom meeting from suspicious timezone
'The alert shows users that join a Zoom meeting from a time zone other than the one the meeting was created in. You can also whitelist known good time zones in the tz_whitelist value using the tz database name format https://en.wikipedia.org/wiki/List_of_tz_database_time_zones'
Show query
let schedule_lookback = 14d;
let join_lookback = 1d;
// If you want to whitelist specific timezones include them in a list here
let tz_whitelist = dynamic([]);
let meetings = (
ZoomLogs
| where TimeGenerated >= ago(schedule_lookback)
| where Event =~ "meeting.created"
| extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)
| extend SchedTimezone = tostring(parse_json(MeetingEvents).Timezone));
ZoomLogs
| where TimeGenerated >= ago(join_lookback)
| where Event =~ "meeting.participant_joined"
| extend JoinedTimeZone = tostring(parse_json(MeetingEvents).Timezone)
| extend MeetingName = tostring(parse_json(MeetingEvents).MeetingName)
| extend MeetingId = tostring(parse_json(MeetingEvents).MeetingId)
| where JoinedTimeZone !in (tz_whitelist)
| join (meetings) on MeetingId
| where SchedTimezone != JoinedTimeZone
| project TimeGenerated, MeetingName, JoiningUser=payload_object_participant_user_name_s, JoinedTimeZone, SchedTimezone, MeetingScheduler=User1
| extend AccountName = tostring(split(JoiningUser, "@")[0]), AccountUPNSuffix = tostring(split(JoiningUser, "@")[1])
Microsoft Sentinel KQL T1078 ↗
User login from different countries within 3 hours (Uses Authentication Normalization)
'This query searches for successful user logins from different countries within 3 hours. To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimAuthentication)'
Show query
let timeframe = ago(3h);
let threshold = 2;
imAuthentication
| where TimeGenerated > timeframe
| where EventType == 'Logon'
    and EventResult == 'Success'
| where isnotempty(SrcGeoCountry)
| summarize
    StartTime        = min(TimeGenerated)
    , EndTime        = max(TimeGenerated)
    , Vendors        = make_set(EventVendor, 128)
    , Products       = make_set(EventProduct, 128)
    , NumOfCountries = dcount(SrcGeoCountry)
    , Countries      = make_set(SrcGeoCountry, 128)
    by TargetUserId, TargetUsername, TargetUserType
| where NumOfCountries >= threshold
| where TargetUserType !in ("Application", "Service", "System", "Other", "Machine", "ServicePrincipal")
| extend
  Name = iif(
      TargetUsername contains "@"
          , tostring(split(TargetUsername, '@', 0)[0])
          , TargetUsername
      ),
  UPNSuffix = iif(
      TargetUsername contains "@"
      , tostring(split(TargetUsername, '@', 1)[0])
      , ""
  )
Microsoft Sentinel KQL T1530 ↗
Users searching for VIP user activity
This query monitors for users running Log Analytics queries that contain filters for specific, defined VIP user accounts or the VIPUser watchlist template. Use this detection to alert for users specifically searching for activity of sensitive users.
Show query
// Replace these with the username or emails of your VIP users you wish to monitor for.
let vips = dynamic(['[email protected]','[email protected]']);
// Add users who are allowed to conduct these searches - this could be specific SOC team members
let allowed_users = dynamic([]);
LAQueryLogs
| where QueryText has_any (vips) or QueryText has_any ('_GetWatchlist("VIPUsers")', "_GetWatchlist('VIPUsers')")
| where AADEmail !in (allowed_users)
| project TimeGenerated, AADEmail, RequestClientApp, QueryText, ResponseRowCount, RequestTarget
| extend timestamp = TimeGenerated, AccountName = tostring(split(AADEmail, "@")[0]), AccountUPNSuffix = tostring(split(AADEmail, "@")[1])
Microsoft Sentinel KQL
Vuln-CVE-2021-40444
Show query
//CVE-2021-40444 hunting. Find device mshtml image load events initiated by common Office executables, then retrieve process events from the device in the same time period where the initiating process is an Office executable, but the process is different

//Data connector required for this query - M365 Defender - Device* tables or Advanced Hunting license

let process = dynamic(["winword.exe", "wordview.exe", "wordpad.exe", "powerpnt.exe", "excel.exe"]);
DeviceImageLoadEvents
| where FileName in ("mshtml.dll", "Microsoft.mshtml.dll")
| where InitiatingProcessFileName in~ (process) 
| project ImageLoadTime=TimeGenerated, DeviceName, InitiatingProcessFolderPath, 
    InitiatingProcessParentFileName, InitiatingProcessParentCreationTime, 
    InitiatingProcessCommandLine
| join kind=inner (
    DeviceProcessEvents)
    on DeviceName
| extend ProcessTime = TimeGenerated
| extend FileNameLower = tolower(FileName)
| extend InitiatingFileNameLower = tolower(InitiatingProcessFileName)
| where InitiatingProcessFileName in~ (process)
| where FileNameLower != InitiatingFileNameLower
| where ProcessTime between ((ImageLoadTime-timespan(5min)).. (ImageLoadTime+timespan(5min)))
| project ImageLoadTime, ProcessTime, DeviceName, InitiatingProcessFileName, FileName
Microsoft Sentinel KQL
Vuln-HighestExposedDevices
Show query
//Summarize your devices by which Microsoft CVEs they are vulnerable too. The data is summarized into severity and ordered by the most exposed devices.

//Data connector required for this query - Advanced Hunting license

//This query only works in Advanced Hunting as the DeviceTvm* tables aren't sent to Sentinel yet
DeviceTvmSoftwareVulnerabilities
| summarize ['Critical Severity Vulnerabilities']=make_set_if(CveId, SoftwareVendor == "microsoft" and VulnerabilitySeverityLevel == "Critical"),
    ['High Severity Vulnerabilities']=make_set_if(CveId, SoftwareVendor == "microsoft" and VulnerabilitySeverityLevel == "High"),
    ['Medium Severity Vulnerabilities']=make_set_if(CveId, SoftwareVendor == "microsoft" and VulnerabilitySeverityLevel == "Medium"), 
    ['Low Severity Vulnerabilities']=make_set_if(CveId, SoftwareVendor == "microsoft" and VulnerabilitySeverityLevel == "Low")
    by DeviceName
| sort by array_length(['Critical Severity Vulnerabilities']) desc
Microsoft Sentinel KQL
Vuln-InternetExposedDevices
Show query
//Use logon and network telemetry to find machines exposed to the internet, then count the critical and high severity vulnerabilities on those devices
//This query only works in Advanced Hunting as the DeviceTvm* tables aren't sent to Sentinel yet

//Data connector required for this query - Advanced Hunting license

//Look for logon events coming from a public IP - query adapted from https://github.com/alexverboon/MDATP/blob/master/AdvancedHunting/Failed%20Logon%20-%20Public%20IP.md
let publicips = 
    DeviceLogonEvents
    | where Timestamp > ago(30d)
    | where ActionType in ("LogonFailed", "LogonSuccess")
    | where RemoteIPType == "Public"
    | distinct RemoteIP;
//Use that same list of IP addresses to search for network traffic coming from those IP's, suggesting the device is available on the internet
let publicdevices=
    DeviceNetworkEvents
    | where Timestamp > ago (30d)
    | where RemoteIPType == "Public"
    | where RemoteIP in (publicips)
    | distinct DeviceName;
//Find the high and critical vulnerability count for those devices
DeviceTvmSoftwareVulnerabilities
| where DeviceName in (publicdevices)
| summarize ['Vulnerability Count']=dcountif(CveId, VulnerabilitySeverityLevel in ("Critical", "High")) by DeviceName
| sort by ['Vulnerability Count'] desc
Microsoft Sentinel KQL
Vuln-KnownExploitableVuln
Show query
//Query the list of Known Exploited Vulnerabilities provided by CISA - https://www.cisa.gov/known-exploited-vulnerabilities-catalog and query your devices for any that are vulnerable

//Data connector required for this query - Advanced Hunting license
//This query only works in Advanced Hunting as the DeviceTvm* tables aren't sent to Sentinel yet

let KEV=
externaldata(cveID: string, vendorProject: string, product: string, vulnerabilityName: string, dateAdded: datetime, shortDescription: string, requiredAction: string, dueDate: datetime, knownRansomwareCampaignUse:string,notes:string)
[
h@'https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv'
]
with(format='csv',ignorefirstrecord=true);
DeviceTvmSoftwareVulnerabilities
| project DeviceName, OSPlatform, cveID=CveId
| join kind=inner KEV on cveID
| summarize ['Vulnerabilities']=make_set(cveID) by DeviceName
| extend ['Count of Known Exploited Vulnerabilities'] = array_length(['Vulnerabilities'])
| sort by ['Count of Known Exploited Vulnerabilities']
Microsoft Sentinel KQL
Vuln-PublicFacingDeviceswithKnownExploitedVuln
Show query
//Query the list of Known Exploited Vulnerabilities provided by CISA - https://www.cisa.gov/known-exploited-vulnerabilities-catalog and find any internet facing devices that are vulnerable

//Data connector required for this query - Advanced Hunting license

let KEV=
externaldata(cveID: string, vendorProject: string, product: string, vulnerabilityName: string, dateAdded: datetime, shortDescription: string, requiredAction: string, dueDate: datetime, knownRansomwareCampaignUse:string,notes:string)
[
h@'https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv'
]
with(format='csv',ignorefirstrecord=true);
let publicdevices=
DeviceInfo
| where IsInternetFacing
| summarize arg_max(Timestamp, *) by DeviceId
| distinct DeviceName;
DeviceTvmSoftwareVulnerabilities
| project DeviceName, OSPlatform, cveID=CveId
| join kind=inner KEV on cveID
| where DeviceName in (publicdevices)
| summarize ['Vulnerabilities']=make_set(cveID), ['Count of Known Exploited Vulnerabilities']=dcount(cveID) by DeviceName
Microsoft Sentinel KQL T1190 ↗
Vulnerable Machines related to OMIGOD CVE-2021-38647
'This query uses the Azure Defender Security Nested Recommendations data to find machines vulnerable to OMIGOD CVE-2021-38647. OMI is the Linux equivalent of Windows WMI and helps users manage configurations across remote and local environments. The query aims to find machines that have this OMI vulnerability (CVE-2021-38647). Security Nested Recommendations data is sent to Microsoft Sentinel using the continuous export feature of Azure Defender(refrence link below). Reference: https://www.wiz
Show query
SecurityNestedRecommendation
| where RemediationDescription has 'CVE-2021-38647'
| parse ResourceDetails with * 'virtualMachines/' VirtualMAchine '"' *
| summarize arg_min(TimeGenerated, *) by TenantId, RecommendationSubscriptionId, VirtualMAchine, RecommendationName,Description,RemediationDescription, tostring(AdditionalData),VulnerabilityId
| extend HostName = tostring(split(VirtualMAchine, ".")[0]), DomainIndex = toint(indexof(VirtualMAchine, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(VirtualMAchine, DomainIndex + 1), VirtualMAchine)
Microsoft Sentinel KQL T1110 ↗
Wazuh - Large Number of Web errors from an IP
'Identifies instances where Wazuh logged over 400 '403' Web Errors from one IP Address. To onboard Wazuh data into Sentinel please view: https://documentation.wazuh.com/current/cloud-security/azure/index.html'
Show query
CommonSecurityLog
| where DeviceProduct =~ "Wazuh"
| where Activity has "Web server 400 error code."
| where Message has "403"
| extend HostName=substring(split(DeviceCustomString1,")")[0],1)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), NumberOfErrors = dcount(SourceIP) by HostName, SourceIP
| where NumberOfErrors > 400
| sort by NumberOfErrors desc
| extend timestamp = StartTime
Microsoft Sentinel KQL T1041 ↗
Windows host username encoded in base64 web request
'This detection will identify network requests in HTTP proxy data that contains Base64 encoded usernames from machines in the DeviceEvents table. This technique was seen usee by POLONIUM in their RunningRAT tool.'
Show query
let accountLookback = 3d;
let requestLookback = 3d;
let extraction_regex = @"(?:\?|&)[a-zA-Z0-9\%]*=([a-zA-Z0-9\/\+\=]*)";
// Collect account names and base64 encode them
DeviceEvents
| where TimeGenerated > ago(accountLookback)
| summarize make_set(DeviceId), make_set(DeviceName) by InitiatingProcessAccountName
| where isnotempty(InitiatingProcessAccountName)
| extend base64_user = base64_encode_tostring(InitiatingProcessAccountName)
| join (
    // Collect requests and extract base64 parameters
    CommonSecurityLog
    | where TimeGenerated > ago(requestLookback)
    | where isnotempty(RequestURL)
    // Summarize early on the RequestURL
    | summarize FirstRequest=min(TimeGenerated), LastRequest=max(TimeGenerated), NumberOfRequests=count() by RequestURL
    | extend base64_candidate = extract_all(extraction_regex, RequestURL)
    | mv-expand base64_candidate  to typeof(string)
) on $left.base64_user == $right.base64_candidate
| project FirstRequest, LastRequest, NumberOfRequests, RequestURL, DeviceIds=set_DeviceId, DeviceNames=set_DeviceName, UserName=InitiatingProcessAccountName
Microsoft Sentinel KQL T1078 ↗
Workspace deletion activity from an infected device
'This query will alert on any sign-ins from devices infected with malware in correlation with workspace deletion activity. Attackers may attempt to delete workspaces containing compute instances after successful compromise to cause service unavailability to regular business operation.'
Show query
SecurityAlert
| where TimeGenerated > ago(1d)
| where ProductName == "Azure Active Directory Identity Protection"
| where AlertName == "Sign-in from an infected device"
| mv-apply EntityAccount=todynamic(Entities) on
(
where EntityAccount.Type == "account"
| extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
)
| mv-apply EntityIp=todynamic(Entities) on
(
where EntityIp.Type == "ip"
| extend IpAddress = tostring(EntityIp.Address)
)
| join kind=inner (
IdentityInfo
| distinct AccountTenantId, AccountObjectId, AccountUPN, AccountDisplayName
| extend UserAccount = AccountUPN
| extend UserName = AccountDisplayName
| where isnotempty(AccountDisplayName) and isnotempty(UserAccount)
| project AccountTenantId, AccountObjectId, UserAccount, UserName
)
on
$left.AadTenantId == $right.AccountTenantId,
$left.AadUserId == $right.AccountObjectId
| extend CompromisedEntity = iff(CompromisedEntity == "N/A" or isempty(CompromisedEntity), UserAccount, CompromisedEntity)
| project  AlertName, AlertSeverity, CompromisedEntity, UserAccount, IpAddress, TimeGenerated, UserName
| join kind=inner 
(
AzureActivity
| where OperationNameValue has_any ("/workspaces/computes/delete", "workspaces/delete") 
| where ActivityStatusValue has_any ("Succeeded", "Success")
| project TimeGenerated, ResourceProviderValue, _ResourceId, SubscriptionId, UserAccount=Caller, IpAddress=CallerIpAddress, CorrelationId, OperationId, ResourceGroup, TenantId
) on IpAddress, UserAccount
| extend AccountName = tostring(split(UserAccount, "@")[0]), AccountUPNSuffix = tostring(split(UserAccount, "@")[1])
Microsoft Sentinel KQL T1040 ↗
Zoom E2E Encryption Disabled
'This alerts when end to end encryption is disabled for Zoom meetings.'
Show query
ZoomLogs
| where Event =~ "account.settings_updated"
| extend NewE2ESetting = columnifexists("payload_object_settings_in_meeting_e2e_encryption_b", "")
| extend OldE2ESetting = columnifexists("payload_old_object_settings_in_meeting_e2e_encryption_b", "")
| where OldE2ESetting =~ 'false' and NewE2ESetting =~ 'true'
| extend AccountName = tostring(split(User, "@")[0]), AccountUPNSuffix = tostring(split(User, "@")[1])
Showing 601-633 of 633