Detection rules › Kusto
Kusto rules: 0g
Cisco SE - Malware outbreak
#'Detects possible malware outbreak.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 225053c7-085b-4fca-a18f-c367f9228bf3
name: Cisco SE - Malware outbreak
description: |
'Detects possible malware outbreak.'
severity: High
requiredDataConnectors:
- connectorId: CiscoSecureEndpoint
dataTypes:
- CiscoSecureEndpoint
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 2;
CiscoSecureEndpoint
| where isnotempty(ThreatName)
| summarize infected = makeset(DstHostname) by ThreatName, bin(TimeGenerated, 10m)
| where array_length(infected) >= threshold
| extend MalwareCustomEntity = ThreatName
entityMappings:
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 2;
Stage 1: source
CiscoSecureEndpoint
Stage 2: where
| where isnotempty(ThreatName)
Stage 3: summarize
| summarize infected = makeset(DstHostname) by ThreatName, bin(TimeGenerated, 10m)
Stage 4: where
| where array_length(infected) >= threshold
Stage 5: extend
| extend MalwareCustomEntity = ThreatName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ThreatName | is_not_null | field:"ThreatName" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ThreatName | summarize |
infected | summarize |
MalwareCustomEntity | extend |
Cloudflare - Multiple error requests from single source
#'Detects multiple failure requests from single source in short timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: ef877d68-755f-4cf1-ac1d-f336e395667d
name: Cloudflare - Multiple error requests from single source
description: |
'Detects multiple failure requests from single source in short timeframe.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 100;
Cloudflare
| where HttpRequestMethod =~ 'GET'
| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
| where err_cnt > threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
Stage 1: source
Cloudflare
Stage 2: where
| where HttpRequestMethod =~ 'GET'
Stage 3: summarize
| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 4: where
| where err_cnt > threshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpRequestMethod | eq |
| field:"HttpRequestMethod" kind:eq value:"GET" |
err_cnt | gt |
| field:"err_cnt" kind:gt value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
err_cnt | summarize |
Configuration Backup Failed
#Detects failed configuration backup operations. This might indicate system or storage issues, or a potential sabotage of the backup infrastructure.
Rule Body
id: 259ef474-836d-4662-86ef-70cb7a38e765
name: Configuration Backup Failed
description: Detects failed configuration backup operations. This might indicate
system or storage issues, or a potential sabotage of the backup infrastructure.
severity: High
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
dataTypes:
- VeeamSessions_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query:
VeeamSessions_CL | where SessionType == "ConfigurationBackup" and ResultStatus == "Failed" | project
TimeGenerated,
VbrHostName,
SessionId = Id,
SessionType,
Name,
Result = ResultStatus
version: 1.0.2
kind: Scheduled
customDetails:
VbrHostName: VbrHostName
SessionId: SessionId
SessionType: SessionType
Name: Name
Result: Result
Stages and Predicates
Stage 1: source
VeeamSessions_CL
Stage 2: where
| where SessionType == "ConfigurationBackup" and ResultStatus == "Failed"
Stage 3: project
| project TimeGenerated, VbrHostName, SessionId = Id, SessionType, Name, Result = ResultStatus
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ResultStatus | eq |
| field:"ResultStatus" kind:eq value:"Failed" |
SessionType | eq |
| field:"SessionType" kind:eq value:"ConfigurationBackup" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Name | project |
Result | project |
SessionId | project |
SessionType | project |
TimeGenerated | project |
VbrHostName | project |
CTE - CTI Blocked URL Attempted
#Cybereinforce blocked a URL/domain access attempt that matched a Cybereinforce Threat Intelligence (CTI) indicator - a higher-confidence signal than a generic policy block.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control |
Rule Body
id: 49da5494-b6b9-42c0-9b9b-b7b219dc8173
name: CTE - CTI Blocked URL Attempted
description: |
Cybereinforce blocked a URL/domain access attempt that matched a Cybereinforce Threat Intelligence (CTI) indicator - a higher-confidence signal than a generic policy block.
severity: Medium
requiredDataConnectors:
- connectorId: cybereinforce_cte
dataTypes:
- CybereinforceCTE_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1189
- T1071
query: |
CybereinforceCTE_CL
| where EventType == "block.url"
| extend d = parse_json(Details)
| extend
DeviceId = tostring(d.deviceId),
HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
DeviceUser = tostring(d.deviceUser),
DeviceOs = tostring(d.deviceOs),
DeviceVersion = tostring(d.deviceVersion),
RuleId = tostring(d.ruleId),
RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
Pattern = tostring(d.pattern),
Url = tostring(d.url),
Action = tostring(d.action),
IsIoc = tobool(d.ioc),
ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
UserAgent = tostring(d.userAgent)
| where RuleName == "Cybereinforce Threat Intelligence" or IsIoc == true
| project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, DeviceVersion, RuleId, RuleName, Pattern, Url, Action, IsIoc, ClientIp, ClientPort, UserAgent, Severity, Category, Source
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CybereinforceCTE_CL
Stage 2: where
| where EventType == "block.url"
Stage 3: extend
| extend d = parse_json(Details)
Stage 4: extend
| extend
DeviceId = tostring(d.deviceId),
HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
DeviceUser = tostring(d.deviceUser),
DeviceOs = tostring(d.deviceOs),
DeviceVersion = tostring(d.deviceVersion),
RuleId = tostring(d.ruleId),
RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
Pattern = tostring(d.pattern),
Url = tostring(d.url),
Action = tostring(d.action),
IsIoc = tobool(d.ioc),
ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
UserAgent = tostring(d.userAgent)
HostName =isnotempty(tostring(d.hostname))tostring(d.hostname)strcat("cte-device-", substring(EntityId, 0, 8))RuleName =isnotempty(tostring(d.ruleName))tostring(d.ruleName)"Pattern Match"Stage 5: where
| where RuleName == "Cybereinforce Threat Intelligence" or IsIoc == true
Stage 6: project
| project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, DeviceVersion, RuleId, RuleName, Pattern, Url, Action, IsIoc, ClientIp, ClientPort, UserAgent, Severity, Category, Source
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"block.url" |
IsIoc | eq |
| field:"IsIoc" kind:eq value:"true" |
RuleName | eq |
| field:"RuleName" kind:eq value:"Cybereinforce Threat Intelligence" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
Category | project |
ClientIp | project |
ClientPort | project |
DeviceId | project |
DeviceOs | project |
DeviceUser | project |
DeviceVersion | project |
HostName | project |
IsIoc | project |
Pattern | project |
RuleId | project |
RuleName | project |
Severity | project |
Source | project |
TenantId | project |
TimeGenerated | project |
Url | project |
UserAgent | project |
Darktrace Incident Event
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
This query searches for Darktrace incident events and creates a Microsoft Sentinel incident for each matching result.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Lateral Movement | |
| Command & Control |
Rule Body
id: 05de0eaf-01bc-4615-99fc-2ec769864b34
name: Darktrace Incident Event
kind: NRT
description: This query searches for Darktrace incident events and creates a Microsoft Sentinel incident for each matching result.
severity: High
requiredDataConnectors:
- connectorId: DarktraceActiveAISecurityPlatform
dataTypes:
- DarktraceIncidents_CL
tactics:
- InitialAccess
- Execution
- LateralMovement
- CommandAndControl
relevantTechniques:
- T1190
- T1059
- T1021
- T1071
query: |
DarktraceIncidents_CL
| where TimeGenerated >= ago(5m)
| extend SentinelSeverity = case(groupCategory == "suspicious", "Medium",
groupCategory == "critical", "High", "Informational")
| extend ProviderName = "Darktrace"
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: accountName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: deviceIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: deviceHostname
customDetails:
Asset: bestAssetName
IncidentEventScore: aiaScore
IncidentScore: groupScore
CurrentGroup: currentGroup
PreviousGroups: groupPreviousGroups
StartTime: startTime
EndTime: endTime
CustomLabel: customLabel
AssetDetails: devices
ModelBreaches: modelBreaches
alertDetailsOverride:
alertDisplayNameFormat: 'Darktrace Incident Event: {{incidentEventTitle}} '
alertDescriptionFormat: '{{summary}}'
alertSeverityColumnName: SentinelSeverity
alertDynamicProperties:
- alertProperty: AlertLink
value: url
- alertProperty: ProductName
value: darktraceProduct
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5M
matchingMethod: AllEntities
version: 1.1.0
Stages and Predicates
Stage 1: source
DarktraceIncidents_CL
Stage 2: where
| where TimeGenerated >= ago(5m)
Stage 3: extend
| extend SentinelSeverity = case(groupCategory == "suspicious", "Medium",
groupCategory == "critical", "High", "Informational")
SentinelSeverity =groupCategory == "suspicious""Medium"groupCategory == "critical""High""Informational"Stage 4: extend
| extend ProviderName = "Darktrace"
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SentinelSeverity | extend |
ProviderName | extend |
Detect Direct Send phishing emails
#In May 2025, a campaign started using the Microsoft Exchange Direct Send feature to send phishing mails to organizations. This feature is designed for use by printers, scanners, cloud services and other devices that need to send messages on behalf of the company. This detection rule tries to detect the malicious mails being send via Direct Send using a couple of indicators: - Sender email is the same as Recipient email - SPF and DMARC both failed - Mail is not comming in via an Exchange connector - The sender IP address country is not a country that the user is known to login from FYI, if you remove the country check (last line of the query), you can use the query to identify if there are legitimate mails as well. If not, you can disable the Direct Send feature in Exchange Online.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
References
- https://www.bleepingcomputer.com/news/security/microsoft-365-direct-send-abused-to-send-phishing-as-internal-users/
- https://www.jumpsec.com/guides/microsoft-direct-send-phishing-abuse-primitive/
- https://techcommunity.microsoft.com/blog/exchange/introducing-more-control-over-direct-send-in-exchange-online/4408790?WT.mc_id=M365-MVP-9501
Telemetry Coverage
Rule Body
let country_land_codes = (
externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string)
['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true)
| distinct Country, Alpha2
);
let login_locations = (
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultSignature == 0
| distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion)
| where isnotempty(LandCode)
| join kind=inner country_land_codes on $left.LandCode == $right.Alpha2
| summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName
);
EmailEvents
| where TimeGenerated > ago(30d)
// Find Direct Send mails
| where SenderMailFromAddress == RecipientEmailAddress
| extend AuthenticationDetails = parse_json(AuthenticationDetails)
| where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail"
// Only flag mails not comming in via an exchange connector (these are legit mails)
| where isempty(Connectors)
// Exclude if detected as phish (since it is already remediated then)
| where DeliveryAction !in ("Junked", "Blocked")
| extend Location = parse_json(geo_info_from_ip_address(SenderIPv4))
| extend MailFromCountry = tostring(Location.country)
// Find the usage countries of the users
| join kind=leftouter login_locations on $left.SenderObjectId == $right.UserId
// Flag when sender country is not a usage country of the user
| where tostring(UsageCountries) !contains MailFromCountry
Stages and Predicates
Let Binding: country_land_codes
let country_land_codes = (
externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string)
['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true)
| distinct Country, Alpha2
);
Let Binding: login_locations
let login_locations = (
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultSignature == 0
| distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion)
| where isnotempty(LandCode)
| join kind=inner country_land_codes on $left.LandCode == $right.Alpha2
| summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName
);
Stage 1: source
EmailEvents
Stage 2: where
| where TimeGenerated > ago(30d)
Stage 3: where
| where SenderMailFromAddress == RecipientEmailAddress
Stage 4: extend
| extend AuthenticationDetails = parse_json(AuthenticationDetails)
Stage 5: where
| where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail"
Stage 6: where
| where isempty(Connectors)
Stage 7: where
| where DeliveryAction !in ("Junked", "Blocked")
Stage 8: extend
| extend Location = parse_json(geo_info_from_ip_address(SenderIPv4))
Stage 9: extend
| extend MailFromCountry = tostring(Location.country)
Stage 10: join
join kind=leftouter (login_locations) on $left.SenderObjectId, $right.UserId
Stage 11: where
where <unknown:tostring(, UsageCountries)> !contains MailFromCountry
Stage 12: summarize aggregation inside the join branch
summarize by UserId, UserPrincipalName
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DeliveryAction | in | Blocked, Junked | excludes:DeliveryAction field:"DeliveryAction" value:"Blocked" field:"DeliveryAction" value:"Junked" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserId | summarize |
UserPrincipalName | summarize |
Excessive Windows Logon Failures
#This query identifies user accounts which has over 50 Windows logon failures today and at least 33% of the count of logon failures over the previous 7 days.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4625: An account failed to log on. |
Rule Body
id: 2391ce61-8c8d-41ac-9723-d945b2e90720
name: Excessive Windows Logon Failures
description: |
'This query identifies user accounts which has over 50 Windows logon failures today and at least 33% of the count of logon failures over the previous 7 days.'
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let starttime = 8d;
let endtime = 1d;
let threshold = 0.333;
let countlimit = 50;
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4625 and AccountType =~ "User"
| where IpAddress !in ("127.0.0.1", "::1")
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), CountToday = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress, Process
| join kind=leftouter (
SecurityEvent
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where EventID == 4625 and AccountType =~ "User"
| where IpAddress !in ("127.0.0.1", "::1")
| summarize CountPrev7day = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
) on EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
| where CountToday >= coalesce(CountPrev7day,0)*threshold and CountToday >= countlimit
//SubStatus Codes are detailed here - https://docs.microsoft.com/windows/security/threat-protection/auditing/event-4625
| extend Reason = case(
SubStatus =~ '0xC000005E', 'There are currently no logon servers available to service the logon request.',
SubStatus =~ '0xC0000064', 'User logon with misspelled or bad user account',
SubStatus =~ '0xC000006A', 'User logon with misspelled or bad password',
SubStatus =~ '0xC000006D', 'Bad user name or password',
SubStatus =~ '0xC000006E', 'Unknown user name or bad password',
SubStatus =~ '0xC000006F', 'User logon outside authorized hours',
SubStatus =~ '0xC0000070', 'User logon from unauthorized workstation',
SubStatus =~ '0xC0000071', 'User logon with expired password',
SubStatus =~ '0xC0000072', 'User logon to account disabled by administrator',
SubStatus =~ '0xC00000DC', 'Indicates the Sam Server was in the wrong state to perform the desired operation',
SubStatus =~ '0xC0000133', 'Clocks between DC and other computer too far out of sync',
SubStatus =~ '0xC000015B', 'The user has not been granted the requested logon type (aka logon right) at this machine',
SubStatus =~ '0xC000018C', 'The logon request failed because the trust relationship between the primary domain and the trusted domain failed',
SubStatus =~ '0xC0000192', 'An attempt was made to logon, but the Netlogon service was not started',
SubStatus =~ '0xC0000193', 'User logon with expired account',
SubStatus =~ '0xC0000224', 'User is required to change password at next logon',
SubStatus =~ '0xC0000225', 'Evidently a bug in Windows and not a risk',
SubStatus =~ '0xC0000234', 'User logon with account locked',
SubStatus =~ '0xC00002EE', 'Failure Reason: An Error occurred during Logon',
SubStatus =~ '0xC0000413', 'Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine',
strcat('Unknown reason substatus: ', SubStatus))
| extend WorkstationName = iff(WorkstationName == "-" or isempty(WorkstationName), Computer , WorkstationName)
| project StartTime, EndTime, EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, Computer, WorkstationName, IpAddress, CountToday, CountPrev7day, Avg7Day = round(CountPrev7day*1.00/7,2), Process
| summarize StartTime = min(StartTime), EndTime = max(EndTime), Computer = make_set(Computer,128), IpAddressList = make_set(IpAddress,128), sum(CountToday), sum(CountPrev7day), avg(Avg7Day)
by EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, WorkstationName, Process
| order by sum_CountToday desc nulls last
| extend timestamp = StartTime, NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0]), HostName = tostring(split(WorkstationName, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(WorkstationName, '.'), 1, -1), '.'))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: WorkstationName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: Process
version: 2.0.3
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 8d;
let endtime = 1d;
let threshold = 0.333;
let countlimit = 50;
Stage 1: source
SecurityEvent
Stage 2: where
| where TimeGenerated >= ago(endtime)
Stage 3: where
| where EventID == 4625 and AccountType =~ "User"
Stage 4: where
| where IpAddress !in ("127.0.0.1", "::1")
Stage 5: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), CountToday = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress, Process
Stage 6: join
| join kind=leftouter (
SecurityEvent
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where EventID == 4625 and AccountType =~ "User"
| where IpAddress !in ("127.0.0.1", "::1")
| summarize CountPrev7day = count() by EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
) on EventID, Account, LogonTypeName, SubStatus, AccountType, Computer, WorkstationName, IpAddress
Stage 7: where
where CountToday >= coalesce((CountPrev7day * 0.333), 0) and CountToday >= 50
Stage 8: extend
extend Reason
Reason =SubStatus == 0xC000005E'There are currently no logon servers available to service the logon request.'SubStatus == 0xC0000064'User logon with misspelled or bad user account'SubStatus == 0xC000006A'User logon with misspelled or bad password'SubStatus == 0xC000006D'Bad user name or password'SubStatus == 0xC000006E'Unknown user name or bad password'SubStatus == 0xC000006F'User logon outside authorized hours'SubStatus == 0xC0000070'User logon from unauthorized workstation'SubStatus == 0xC0000071'User logon with expired password'SubStatus == 0xC0000072'User logon to account disabled by administrator'SubStatus == 0xC00000DC'Indicates the Sam Server was in the wrong state to perform the desired operation'SubStatus == 0xC0000133'Clocks between DC and other computer too far out of sync'SubStatus == 0xC000015B'The user has not been granted the requested logon type (aka logon right) at this machine'SubStatus == 0xC000018C'The logon request failed because the trust relationship between the primary domain and the trusted domain failed'SubStatus == 0xC0000192'An attempt was made to logon, but the Netlogon service was not started'SubStatus == 0xC0000193'User logon with expired account'SubStatus == 0xC0000224'User is required to change password at next logon'SubStatus == 0xC0000225'Evidently a bug in Windows and not a risk'SubStatus == 0xC0000234'User logon with account locked'SubStatus == 0xC00002EE'Failure Reason: An Error occurred during Logon'SubStatus == 0xC0000413'Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine'strcat('Unknown reason substatus: ', SubStatus)Stage 9: extend
extend WorkstationName
WorkstationName =(WorkstationName == "-" or isempty(WorkstationName))ComputerWorkstationNameStage 10: project
project Account, AccountType, Avg7Day, Computer, CountPrev7day, CountToday, EndTime, EventID, IpAddress, LogonTypeName, Process, Reason, StartTime, SubStatus, WorkstationName
Stage 11: summarize
summarize Computer, EndTime, IpAddressList, StartTime by EventID, Account, LogonTypeName, SubStatus, Reason, AccountType, WorkstationName, Process
Stage 12: sort
sort by sum_CountToday
Stage 13: extend
extend DnsDomain, HostName, NTDomain, Name, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountType | eq |
| field:"AccountType" kind:eq value:"User" |
CountToday | ge |
| field:"CountToday" kind:ge value:"50" |
EventID | eq |
| field:"EventID" kind:eq value:"4625" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IpAddress | in | 127.0.0.1, ::1 | excludes:IpAddress field:"IpAddress" value:"127.0.0.1" field:"IpAddress" value:"::1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | summarize |
AccountType | summarize |
Computer | summarize |
EndTime | summarize |
EventID | summarize |
IpAddressList | summarize |
LogonTypeName | summarize |
Process | summarize |
Reason | summarize |
StartTime | summarize |
SubStatus | summarize |
WorkstationName | summarize |
DnsDomain | extend |
HostName | extend |
NTDomain | extend |
Name | extend |
timestamp | extend |
High bandwidth in the network (Microsoft Defender for IoT)
#'This alert leverages Defender for IoT to detect an unusually high bandwidth which may be an indication of a new service/process or malicious activity on the network. An example scenario is a cyber threat attempting to manipulate the SCADA network.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: caa4665f-21fa-462d-bb31-92226e746c68
name: High bandwidth in the network (Microsoft Defender for IoT)
description: |
'This alert leverages Defender for IoT to detect an unusually high bandwidth which may be an indication of a new service/process or malicious activity on the network. An example scenario is a cyber threat attempting to manipulate the SCADA network.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: IoT
dataTypes:
- SecurityAlert (ASC for IoT)
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T0842
query: |
SecurityAlert
| where ProviderName == "IoTSecurity"
| where AlertName in ("Abnormal Traffic Bandwidth", "Abnormal Traffic Bandwidth Between Devices", "ARP Spoofing", "ICMP Flooding")
| extend ExtendedProperties = parse_json(ExtendedProperties)
| where tostring(ExtendedProperties.isNew) == "True"
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
entityMappings:
sentinelEntitiesMappings:
- columnName: Entities
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
Sensor: DeviceId
Protocol: Protocol
VendorOriginalId: VendorOriginalId
AlertManagementUri: AlertManagementUri
alertDetailsOverride:
alertDisplayNameFormat: (MDIoT) {{AlertName}}
alertDescriptionFormat: (MDIoT) {{Description}}
alertTacticsColumnName: Tactics
alertSeverityColumnName: AlertSeverity
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: RemediationSteps
value: RemediationSteps
- alertProperty: Techniques
value: Techniques
- alertProperty: ProductComponentName
value: ProductComponentName
- alertProperty: AlertLink
value: AlertLink
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where ProviderName == "IoTSecurity"
Stage 3: where
| where AlertName in ("Abnormal Traffic Bandwidth", "Abnormal Traffic Bandwidth Between Devices", "ARP Spoofing", "ICMP Flooding")
Stage 4: extend
| extend ExtendedProperties = parse_json(ExtendedProperties)
Stage 5: where
| where tostring(ExtendedProperties.isNew) == "True"
Stage 6: extend
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
Stage 7: project
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | in |
| field:"AlertName" kind:in |
ExtendedProperties.isNew | eq |
| field:"ExtendedProperties.isNew" kind:eq value:"True" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IoTSecurity" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertManagementUri | project |
AlertName | project |
AlertSeverity | project |
Description | project |
DestDeviceAddress | project |
DeviceId | project |
Entities | project |
ProductComponentName | project |
ProductName | project |
Protocol | project |
RemediationSteps | project |
SourceDeviceAddress | project |
Tactics | project |
Techniques | project |
TimeGenerated | project |
VendorOriginalId | project |
Insider Risk_High User Security Incidents Correlation
#'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see Investigate incidents with Microsoft Sentinel.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Rule Body
id: 28a75d10-9b75-4192-9863-e452c3ad24db
name: Insider Risk_High User Security Incidents Correlation
description: |
'This alert joins SecurityAlerts to SecurityIncidents to associate Security Alerts and Incidents with user accounts. This aligns all Microsoft Alerting Products with Microsoft Incident Generating Products (Microsoft Sentinel, M365 Defender) for a count of user security incidents over time. The default threshold is 5 security incidents, and this is customizable per the organization's requirements. Results include UserPrincipalName (UPN), SecurityIncident, LastIncident, ProductName, LastObservedTime, and Previous Incidents. There is an option for configuration of correlations against Microsoft Sentinel watchlists. For more information, see [Investigate incidents with Microsoft Sentinel]( https://docs.microsoft.com/azure/sentinel/investigate-cases).'
severity: High
requiredDataConnectors:
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert (MDATP)
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert (IPC)
- connectorId: AzureSecurityCenter
dataTypes:
- SecurityAlert (ASC)
- connectorId: IoT
dataTypes:
- SecurityAlert (ASC for IoT)
- connectorId: MicrosoftCloudAppSecurity
dataTypes:
- SecurityAlert (ASC for IoT)
- connectorId: IoT
dataTypes:
- SecurityAlert (MCAS)
- connectorId: OfficeATP
dataTypes:
- SecurityAlert (Office 365)
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
relevantTechniques:
- T1204
query: |
let PreviousIncidents =
SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]),
Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize PreviousIncidents = make_set(IncidentNumber) by UPN;
let LastTimeObserved =
SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]),
Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize arg_max(TimeGenerated, IncidentName) by UPN;
SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]),
Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize count() by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName
| extend SecurityIncidents = count_
| where SecurityIncidents > 5 //Adjust & Tune within Organzational Requirements
| join (LastTimeObserved) on UPN
| project-rename LastObserved = TimeGenerated, LastIncident = IncidentNumber
| project-away IncidentName, count_, UPN1, IncidentName1, Severity, IncidentUrl
| join kind=inner (PreviousIncidents) on UPN
// | lookup kind=inner _GetWatchlist('<Your Watchlist Name>') on $left.UPN == $right.SearchKey
| project UPN, SecurityIncidents, LastIncident, ProductName, LastObserved, PreviousIncidents
| sort by SecurityIncidents desc
| extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1])
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: true
lookbackDuration: 3d
matchingMethod: Selected
groupByEntities:
- Account
version: 1.1.2
kind: Scheduled
Stages and Predicates
Let Binding: PreviousIncidents
let PreviousIncidents = SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]),
Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize PreviousIncidents = make_set(IncidentNumber) by UPN;
Let Binding: LastTimeObserved
let LastTimeObserved = SecurityIncident
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
| mv-expand AlertIds
| extend AlertId = tostring(AlertIds)
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
| summarize hint.strategy = shuffle arg_max(TimeGenerated, *), NumberOfUpdates = count() by SystemAlertId
| mv-expand todynamic(Entities)
| where Entities["Type"] =~ "account"
| extend Name = tostring(tolower(Entities["Name"])), NTDomain = tostring(Entities["NTDomain"]), UPNSuffix = tostring(Entities["UPNSuffix"]), AadUserId = tostring(Entities["AadUserId"]), AadTenantId = tostring(Entities["AadTenantId"]),
Sid = tostring(Entities["Sid"]), IsDomainJoined = tobool(Entities["IsDomainJoined"]), Host = tostring(Entities["Host"])
| extend UPN = iff(Name != "" and UPNSuffix != "", strcat(Name, "@", UPNSuffix), "")
| extend Href_ = tostring(parse_json(ExtendedLinks)[0].Href)
| where UPN <> ""
| summarize arg_max(TimeGenerated, IncidentName) by UPN;
Stage 1: source
SecurityIncident
Stage 2: summarize
| summarize hint.strategy = shuffle arg_max(LastModifiedTime, *) by IncidentNumber
Stage 3: mv-expand
| mv-expand AlertIds
Stage 4: extend
| extend AlertId = tostring(AlertIds)
Stage 5: join
| join kind= innerunique (
SecurityAlert
)
on $left.AlertId == $right.SystemAlertId
Stage 6: summarize
summarize NumberOfUpdates by SystemAlertId
Stage 7: mv-expand
mv-expand
Stage 8: where
where Entities["Type"] =~ "account"
Stage 9: extend (3 consecutive steps)
extend AadTenantId, AadUserId, Host, Href_, IsDomainJoined, NTDomain, Name, Sid, UPN, UPNSuffix
Stage 10: where
where UPN != ""
Stage 11: summarize
summarize by UPN, IncidentName, IncidentNumber, IncidentUrl, Severity, ProductName
Stage 12: extend
extend SecurityIncidents
Stage 13: where
where SecurityIncidents > 5
Stage 14: join
join (LastTimeObserved) on UPN
Stage 15: project-rename
project-rename
Stage 16: project-away
project-away IncidentName, IncidentName1, IncidentUrl, Severity, UPN1, count_
Stage 17: join
join kind=inner (PreviousIncidents) on UPN
Stage 18: project
project LastIncident, LastObserved, PreviousIncidents, ProductName, SecurityIncidents, UPN
Stage 19: sort
sort by SecurityIncidents
Stage 20: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Entities["Type"] | eq |
| |
SecurityIncidents | gt |
| field:"SecurityIncidents" kind:gt value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
LastIncident | project |
LastObserved | project |
PreviousIncidents | project |
ProductName | project |
SecurityIncidents | project |
UPN | project |
AccountName | extend |
AccountUPNSuffix | extend |
Keeper Security - Password Changed
#'Creates an informational incident based on Keeper Security Password Changed data in Microsoft Sentinel'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: f031fbbc-37d8-4667-b795-d386bf2b5ab2
name: Keeper Security - Password Changed
description: |
'Creates an informational incident based on Keeper Security Password Changed data in Microsoft Sentinel'
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: KeeperSecurityPush2
dataTypes:
- KeeperSecurityEventNewLogs_CL
suppressionDuration: PT5H
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
tactics:
- Persistence
relevantTechniques:
- T1556
query: |
KeeperSecurityEventNewLogs_CL
| where AuditEvent == "change_master_password"
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: "{{AuditEvent}} on {{RemoteAddress}}"
alertDescriptionFormat: "{{AuditEvent}} has been captured in the Keeper Security Event Logs"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Username
- entityType: IP
fieldMappings:
- identifier: Address
columnName: RemoteAddress
version: 1.0.3
kind: NRT
Stages and Predicates
Stage 1: source
KeeperSecurityEventNewLogs_CL
Stage 2: where
| where AuditEvent == "change_master_password"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AuditEvent | eq |
| field:"AuditEvent" kind:eq value:"change_master_password" |
Ransomware Attack Detected
#Identifies ransomware attacks detected by the Ransomware Protection service running on a Nasuni Edge Appliance.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 6c8770fb-c854-403e-a64d-0293ba344d5f
name: Ransomware Attack Detected
description: 'Identifies ransomware attacks detected by the Ransomware Protection service running on a Nasuni Edge Appliance.'
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Impact
relevantTechniques:
- T1486
query: |-
Syslog
| project TimeGenerated, Computer, SyslogMessage
| where SyslogMessage has "The Filer has detected a new ransomware attack"
| extend pattern = substring(SyslogMessage, indexof(SyslogMessage, "(")+1, indexof(SyslogMessage, ")") - indexof(SyslogMessage,"(")-1)
| extend volume_name = substring(SyslogMessage, indexof(SyslogMessage, "volume")+7, indexof(SyslogMessage,". Visit") - (indexof(SyslogMessage, "volume")+7))
| sort by TimeGenerated desc
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertnameFormat: 'Nasuni: Ransomware Attack Detected'
alertDescriptionFormat: Ransomware attack detected by Nasuni at {{TimeGenerated}}.
alertDynamicProperties:
- alertProperty: RemediationSteps
value: SyslogMessage
customDetails:
VolumeName: volume_name
entityMappings:
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: pattern
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.3
Stages and Predicates
Stage 1: source
Syslog
Stage 2: project
| project TimeGenerated, Computer, SyslogMessage
Stage 3: where
| where SyslogMessage has "The Filer has detected a new ransomware attack"
Stage 4: extend
| extend pattern = substring(SyslogMessage, indexof(SyslogMessage, "(")+1, indexof(SyslogMessage, ")") - indexof(SyslogMessage,"(")-1)
Stage 5: extend
| extend volume_name = substring(SyslogMessage, indexof(SyslogMessage, "volume")+7, indexof(SyslogMessage,". Visit") - (indexof(SyslogMessage, "volume")+7))
Stage 6: sort
| sort by TimeGenerated desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"The Filer has detected a new ransomware attack" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | project |
SyslogMessage | project |
TimeGenerated | project |
pattern | extend |
volume_name | extend |
Tailscale: External (shared-in) device added
#Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: b2c3d4e5-6789-0123-4567-890123450043
name: "Tailscale: External (shared-in) device added"
description: |
Identifies new external (shared-in) devices joining the tailnet that were not present in the prior 24-hour baseline. Each shared-in device expands the trust boundary - confirm the share matches a documented agreement and ACL scope.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let recent =
Tailscale_Devices_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags;
let prior =
Tailscale_Devices_CL
| where TimeGenerated between (ago(2d) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| distinct DeviceId;
recent
| join kind=leftanti prior on DeviceId
| project TimeGenerated = now(), DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
let recent is inlined into the numbered stages below.
Let Binding: prior
let prior = Tailscale_Devices_CL
| where TimeGenerated between (ago(2d) .. ago(1h))
| summarize arg_max(TimeGenerated, *) by DeviceId
| where IsExternal == true
| distinct DeviceId;
Stages 1 to 5 define let recent as the rule's main pipeline source. Stages 6 to 7 use it.
Stage 1: source
Tailscale_Devices_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by DeviceId
Stage 4: where
| where IsExternal == true
Stage 5: project
| project DeviceId, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, Tags
Stage 6: join (negated)
join kind=leftanti (prior) on DeviceId
Stage 7: project
project ClientVersion, Created, DeviceName, Hostname, LastSeen, Os, Tags, TimeGenerated, User
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IsExternal | eq |
| field:"IsExternal" kind:eq value:"true" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IsExternal | eq | true | excludes:IsExternal field:"IsExternal" value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientVersion | project |
Created | project |
DeviceName | project |
Hostname | project |
LastSeen | project |
Os | project |
Tags | project |
TimeGenerated | project |
User | project |
Tenant Password Changed
#Detects when a tenant password is updated.
Rule Body
id: afec0070-2b46-4366-a272-e2d9cc8a50f2
name: Tenant Password Changed
description: Detects when a tenant password is updated.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 24114\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 24114
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"24114" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
ThreatConnect TI Map URL Entity to OfficeActivity Data
#'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 12c3b31b-66a6-53ff-b6ab-6ae45e56dc92
name: ThreatConnect TI Map URL Entity to OfficeActivity Data
version: 1.0.1
kind: Scheduled
description: |
'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where ExpirationDateTime > now() and Active == true
// Filter out non ThreatConnect TI Sources
| where SourceSystem startswith "ThreatConnect-"
// Picking up only IOC's that contain the entities we want
| where isnotempty(Url)
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (
OfficeActivity
| where TimeGenerated >= ago(dt_lookBack)
//Extract the Url from a number of potential fields
| extend Url = iif(OfficeWorkload == "AzureActiveDirectory",extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+);", 1,ModifiedProperties),tostring(parse_json(ModifiedProperties)[12].NewValue))
| where isnotempty(Url)
// Ensure we get a clean URL
| extend Url = tostring(split(Url, ';')[0])
| extend OfficeActivity_TimeGenerated = TimeGenerated
// Project a single user identity that we can use for entity mapping
| extend User = iif(isnotempty(UserId), UserId, iif(isnotempty(Actor), tostring(parse_json(Actor)[0].ID), tostring(parse_json(Parameters)[0].Value)))
) on Url
| where OfficeActivity_TimeGenerated < ExpirationDateTime
| summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, Url
| project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Operation,
UserType, OfficeWorkload, Parameters, Url, User
| extend timestamp = OfficeActivity_TimeGenerated, Name = tostring(split(User, '@', 0)[0]), UPNSuffix = tostring(split(User, '@', 1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 3: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 4: where
| where ExpirationDateTime > now() and Active == true
Stage 5: where
| where SourceSystem startswith "ThreatConnect-"
Stage 6: where
| where isnotempty(Url)
Stage 7: join
| join kind=innerunique (
OfficeActivity
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = iif(OfficeWorkload == "AzureActiveDirectory",extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+);", 1,ModifiedProperties),tostring(parse_json(ModifiedProperties)[12].NewValue))
| where isnotempty(Url)
| extend Url = tostring(split(Url, ';')[0])
| extend OfficeActivity_TimeGenerated = TimeGenerated
| extend User = iif(isnotempty(UserId), UserId, iif(isnotempty(Actor), tostring(parse_json(Actor)[0].ID), tostring(parse_json(Parameters)[0].Value)))
) on Url
Stage 8: where
where OfficeActivity_TimeGenerated < ExpirationDateTime
Stage 9: summarize
summarize OfficeActivity_TimeGenerated by IndicatorId, Url
Stage 10: project
project ActivityGroupNames, ConfidenceScore, Description, ExpirationDateTime, IndicatorId, OfficeActivity_TimeGenerated, OfficeWorkload, Operation, Parameters, ThreatType, Url, User, UserType
Stage 11: extend
extend Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
SourceSystem | starts_with |
| field:"SourceSystem" kind:starts_with value:"ThreatConnect-" |
Url | is_not_null | field:"Url" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceScore | project |
Description | project |
ExpirationDateTime | project |
IndicatorId | project |
OfficeActivity_TimeGenerated | project |
OfficeWorkload | project |
Operation | project |
Parameters | project |
ThreatType | project |
Url | project |
User | project |
UserType | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
TI map Domain entity to EmailEvents
#Identifies a match in EmailEvents table from any Domain IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | EmailEvents action any: Email processed |
Rule Body
id: 96307710-8bb9-4b45-8363-a90c72ebf86f
name: TI map Domain entity to EmailEvents
description: |
Identifies a match in EmailEvents table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- EmailEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));
let SenderDomains = EmailEvents_ | distinct SenderFromDomain | summarize make_list(SenderFromDomain);
let RecipientDomains = EmailEvents_ | distinct RecipientEmailDomain | summarize make_list(RecipientEmailDomain);
let TI = materialize(ThreatIntelligenceIndicator
| where TimeGenerated >= ago(ioc_lookBack)
| where isnotempty(DomainName)
| extend TI_Domain = tolower(DomainName)
| where TI_Domain in (SenderDomains) or TI_Domain in (RecipientDomains)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
(union
(TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.SenderFromDomain),
(TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.RecipientEmailDomain))
| where EmailEvents_TimeGenerated < ExpirationDateTime
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by IndicatorId, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_Domain, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: RecipientEmailAddress
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: EmailEvents_
let EmailEvents_ = materialize(EmailEvents | where isnotempty(RecipientEmailAddress) and isnotempty(SenderFromAddress) and TimeGenerated >= ago(dt_lookBack) and DeliveryAction !has "Blocked" | project-rename EmailEvents_TimeGenerated = TimeGenerated | extend SenderFromDomain = tolower(SenderFromDomain) | extend RecipientEmailDomain = tolower(tostring(split(RecipientEmailAddress, '@', 1))));
Let Binding: SenderDomains
let SenderDomains = EmailEvents_ | distinct SenderFromDomain | summarize make_list(SenderFromDomain);
Let Binding: RecipientDomains
let RecipientDomains = EmailEvents_ | distinct RecipientEmailDomain | summarize make_list(RecipientEmailDomain);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: TI, TI
Leg 1: TI
TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.SenderFromDomain
Leg 2: TI
TI | join kind=innerunique (EmailEvents_) on $left.TI_Domain == $right.RecipientEmailDomain
Applied to the Combined Result
| where EmailEvents_TimeGenerated < ExpirationDateTime
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by IndicatorId, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Url, Type, TI_Domain, TrafficLightProtocolLevel, DeliveryAction, DeliveryLocation, EmailDirection
| extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0])
| extend timestamp = EmailEvents_TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
RecipientEmailAddress | is_not_null | field:"RecipientEmailAddress" kind:is_not_null | |
SenderFromAddress | is_not_null | field:"SenderFromAddress" kind:is_not_null | |
TI_Domain | in |
| field:"TI_Domain" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
DeliveryAction | match | Blocked | excludes:DeliveryAction field:"DeliveryAction" value:"Blocked" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceLevel | project |
ConfidenceScore | project |
DeliveryAction | project |
DeliveryLocation | project |
Description | project |
DomainName | project |
EmailDirection | project |
EmailEvents_TimeGenerated | project |
ExpirationDateTime | project |
IndicatorId | project |
RecipientEmailAddress | project |
SenderFromAddress | project |
Subject | project |
TI_Domain | project |
ThreatType | project |
TrafficLightProtocolLevel | project |
Type | project |
Url | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
UniFi Site Manager: Firmware Update Available
#Identifies UniFi devices with firmware updates available. Keeping firmware patched is critical as updates often include security vulnerability fixes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 83b88ab5-21ca-5dd2-df91-6db4354f9360
name: "UniFi Site Manager: Firmware Update Available"
description: |
Identifies UniFi devices with firmware updates available. Keeping firmware patched is critical as updates often include security vulnerability fixes.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Devices_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
subTechniques: []
query: |
Unifi_SiteManager_Devices_CL
| where TimeGenerated > ago(1d)
| summarize arg_max(TimeGenerated, *) by Id
| where isnotempty(UpdateAvailable)
| extend DeviceDetail = strcat(coalesce(Name, "Unnamed"), " (", Model, ") ", Version, " -> ", UpdateAvailable, " @", Ip)
| summarize
DevicesNeedingUpdate = count(),
DeviceList = make_list(DeviceDetail),
Models = make_set(Model),
ProductLines = make_set(ProductLine)
| where DevicesNeedingUpdate >= 1
| extend
TimeGenerated = now(),
AffectedModels = strcat_array(Models, ", "),
AffectedProductLines = strcat_array(ProductLines, ", "),
DeviceListString = strcat_array(DeviceList, "; ")
| extend Activity = strcat(DevicesNeedingUpdate, ' device(s) have firmware updates available: ', AffectedModels)
| project
TimeGenerated,
DevicesNeedingUpdate,
AffectedModels,
AffectedProductLines,
Activity,
DeviceListString
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: AffectedProductLines
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P7D
matchingMethod: AnyAlert
kind: Scheduled
version: 1.0.2
Stages and Predicates
Stage 1: source
Unifi_SiteManager_Devices_CL
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by Id
Stage 4: where
| where isnotempty(UpdateAvailable)
Stage 5: extend
| extend DeviceDetail = strcat(coalesce(Name, "Unnamed"), " (", Model, ") ", Version, " -> ", UpdateAvailable, " @", Ip)
Stage 6: summarize
| summarize
DevicesNeedingUpdate = count(),
DeviceList = make_list(DeviceDetail),
Models = make_set(Model),
ProductLines = make_set(ProductLine)
Stage 7: where
| where DevicesNeedingUpdate >= 1
Stage 8: extend
| extend
TimeGenerated = now(),
AffectedModels = strcat_array(Models, ", "),
AffectedProductLines = strcat_array(ProductLines, ", "),
DeviceListString = strcat_array(DeviceList, "; ")
Stage 9: extend
| extend Activity = strcat(DevicesNeedingUpdate, ' device(s) have firmware updates available: ', AffectedModels)
Stage 10: project
| project
TimeGenerated,
DevicesNeedingUpdate,
AffectedModels,
AffectedProductLines,
Activity,
DeviceListString
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DevicesNeedingUpdate | ge |
| field:"DevicesNeedingUpdate" kind:ge value:"1" |
UpdateAvailable | is_not_null | field:"UpdateAvailable" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
AffectedModels | project |
AffectedProductLines | project |
DeviceListString | project |
DevicesNeedingUpdate | project |
TimeGenerated | project |
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.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Impact |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityInfo action any: Identity information |
Rule Body
id: a5b3429d-f1da-42b9-883c-327ecb7b91ff
name: Workspace deletion activity from an infected device
description: |
'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.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert (IPC)
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Impact
relevantTechniques:
- T1078
- T1489
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])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserAccount
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: _ResourceId
version: 1.0.8
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Threat Protection", "Platform" ]
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where ProductName == "Azure Active Directory Identity Protection"
Stage 4: where
| where AlertName == "Sign-in from an infected device"
Stage 5: kusto:mv-apply
| mv-apply EntityAccount=todynamic(Entities) on
(
where EntityAccount.Type == "account"
| extend AadTenantId = tostring(EntityAccount.AadTenantId), AadUserId = tostring(EntityAccount.AadUserId)
)
Stage 6: kusto:mv-apply
| mv-apply EntityIp=todynamic(Entities) on
(
where EntityIp.Type == "ip"
| extend IpAddress = tostring(EntityIp.Address)
)
Stage 7: join
| 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
Stage 8: extend
extend CompromisedEntity
CompromisedEntity =(CompromisedEntity == "N/A" or isempty(CompromisedEntity))UserAccountCompromisedEntityStage 9: project
project AlertName, AlertSeverity, CompromisedEntity, IpAddress, TimeGenerated, UserAccount, UserName
Stage 10: join
| 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
Stage 11: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountDisplayName | is_not_null | field:"AccountDisplayName" kind:is_not_null | |
ActivityStatusValue | match |
| field:"ActivityStatusValue" kind:match |
AlertName | eq |
| field:"AlertName" kind:eq value:"Sign-in from an infected device" |
EntityAccount.Type | eq |
| field:"EntityAccount.Type" kind:eq value:"account" |
EntityIp.Type | eq |
| field:"EntityIp.Type" kind:eq value:"ip" |
OperationNameValue | match |
| field:"OperationNameValue" kind:match |
ProductName | eq |
| field:"ProductName" kind:eq value:"Azure Active Directory Identity Protection" |
UserAccount | is_not_null | field:"UserAccount" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertName | project |
AlertSeverity | project |
CompromisedEntity | project |
IpAddress | project |
TimeGenerated | project |
UserAccount | project |
UserName | project |
AccountName | extend |
AccountUPNSuffix | extend |