Detection rules › Kusto
Kusto rules: 0o
1Password - User added to privileged group
#This will alert when a user is added to a privileged group which has been implemented by an actor that was not the target user account. Once the analytics rule is triggered it will group all related future alerts for upto 30 minutes when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | gm-join: Join Group |
Rule Body
id: 849ea271-cd9c-4afe-a13b-ddbbac5fc6d3
name: 1Password - User added to privileged group
version: 1.0.0
kind: Scheduled
description: |-
This will alert when a user is added to a privileged group which has been implemented by an actor that was not the target user account. Once the analytics rule is triggered it will group all related future alerts for upto 30 minutes when all related entities are the same.
Ref: https://1password.com/
Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
- connectorId: 1Password
dataTypes:
- OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |-
let watchlist =
_GetWatchlist("PG1PW")
| project SearchKey
;
let groups = dynamic([""]);
OnePasswordEventLogs_CL
| where log_source == "auditevents"
| where action == "join"
| where object_type == "gm"
| where tostring(actor_details.email) != tostring(aux_details.email)
// Enable the line below when using the "Privileged Groups - 1PW" watchlist
| where object_uuid in (watchlist)
// Enable the line below when using the dynamic groups list within the analytics rule itself
// | where object_uuid in (groups)
| extend
TargetUsername = aux_details.email
, ActorUsername = actor_details.email
, SrcIpAddr = session.ip
, GroupRole = case(
aux_info == "R", "Group member"
, aux_info == "A", "Group manager"
, aux_info
)
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 30m
matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUsername
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
Stages and Predicates
Parameters
let groups = dynamic([""]);
Let Binding: watchlist
let watchlist = _GetWatchlist("PG1PW")
| project SearchKey;
Stage 1: source
OnePasswordEventLogs_CL
Stage 2: where
| where log_source == "auditevents"
Stage 3: where
| where action == "join"
Stage 4: where
| where object_type == "gm"
Stage 5: where
| where tostring(actor_details.email) != tostring(aux_details.email)
Stage 6: where
| where object_uuid in (watchlist)
Stage 7: extend
| extend
TargetUsername = aux_details.email
, ActorUsername = actor_details.email
, SrcIpAddr = session.ip
, GroupRole = case(
aux_info == "R", "Group member"
, aux_info == "A", "Group manager"
, aux_info
)
GroupRole =aux_info == "R""Group member"aux_info == "A""Group manager"aux_infoIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"join" |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq value:"gm" |
object_uuid | in |
| field:"object_uuid" kind:in value:"watchlist" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
GroupRole | extend |
SrcIpAddr | extend |
TargetUsername | extend |
Acronis - Multiple Endpoints Accessing Malicious URLs
#Multiple endpoints accessing malicious URLs could indicate an ongoing phishing attack, with several employees interacting with those URLs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Rule Body
id: 1385f0ce-69d9-4abf-8039-52080c8c7017
name: Acronis - Multiple Endpoints Accessing Malicious URLs
description: 'Multiple endpoints accessing malicious URLs could indicate an ongoing phishing attack, with several employees interacting with those URLs.'
severity: Medium
query: |
CommonSecurityLog
| where DeviceVendor == "Acronis"
| where DeviceEventClassID == "MaliciousUrlDetected"
| summarize MaliciousUrlDetected = count() by DeviceName
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 2
tactics:
- Execution
relevantTechniques:
- T1204.001
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: true
lookbackDuration: P1D
matchingMethod: AnyAlert
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
DeviceName: DeviceName
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
version: 1.0.0
kind: Scheduled
requiredDataConnectors: []
Stages and Predicates
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "Acronis"
Stage 3: where
| where DeviceEventClassID == "MaliciousUrlDetected"
Stage 4: summarize
| summarize MaliciousUrlDetected = count() by DeviceName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"MaliciousUrlDetected" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Acronis" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceName | summarize |
MaliciousUrlDetected | summarize |
BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName Privileges
#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.
BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName Privileges
Rule Body
id: 8bbc3980-8b18-40c5-829e-d282afa52284
name: BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName
Privileges
description: BloodHound Attack Path Finding - Large Default Groups With WriteServicePrincipalName
Privileges
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "LargeDefaultGroupsWriteSPN"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "LargeDefaultGroupsWriteSPN"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"LargeDefaultGroupsWriteSPN" |
Cloudflare - WAF Allowed threat
#'Detects WAF "Allowed" action on threat events.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: f53fe2a9-96b5-454c-827e-cf1764a67fb0
name: Cloudflare - WAF Allowed threat
description: |
'Detects WAF "Allowed" action on threat events.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
Cloudflare
| where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)
| where WAFAction =~ 'Allow'
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: CompleteUrl
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
Cloudflare
Stage 2: where
| where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)
Stage 3: where
| where WAFAction =~ 'Allow'
Stage 4: extend
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
WAFAction | eq |
| field:"WAFAction" kind:eq value:"Allow" |
WAFRuleID | is_not_null | field:"WAFRuleID" kind:is_not_null | |
WAFRuleMessage | is_not_null | field:"WAFRuleMessage" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CompleteUrl | extend |
SAP BTP - Malware detected in BAS dev space
#Identifies instances of malware detected using SAP internal malware agent within Business Application Studio dev spaces.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Execution | |
| Persistence |
Rule Body
id: 31997e9a-7447-47f3-8208-4f5d7efe497c
kind: Scheduled
name: SAP BTP - Malware detected in BAS dev space
description: Identifies instances of malware detected using SAP internal malware agent
within Business Application Studio dev spaces.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPBTPAuditEvents
dataTypes:
- SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- ResourceDevelopment
- Execution
- Persistence
relevantTechniques:
- T1584
- T1072
- T0873
query: |
SAPBTPAuditLog_CL
| where Message has "malware"
| extend MessageData = parse_json(tostring(Message.data))
| extend MalwareData = parse_json(tostring(MessageData.message))
| extend
ClusterID = tostring(MessageData.clusterID),
WorkspaceID = tostring(MessageData.wsID),
DevSpaceId = tostring(MalwareData.dev_space_id),
User = tostring(MalwareData.user),
Malware = strcat_array(MalwareData.findings, ", "),
MessageText = tostring(MalwareData.message)
| extend
AccountName = tostring(split(User, '@')[0]),
UPNSuffix = tostring(split(User, '@')[1])
| where isnotempty(User) and isnotempty(Malware)
| project
UpdatedOn,
ClusterID,
WorkspaceID,
DevSpaceId,
MessageText,
User,
Malware,
Tenant,
SpaceId,
Category,
CloudApp = "SAP BTP",
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: CloudApp
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: Malware
alertDetailsOverride:
alertDisplayNameFormat: SAP BTP - Malware detected in Business Apps Studio dev space
alertDescriptionFormat: 'Malware was found in the following subaccount: {{Tenant}}'
version: 3.0.8
Stages and Predicates
Stage 1: source
SAPBTPAuditLog_CL
Stage 2: where
| where Message has "malware"
Stage 3: extend (4 consecutive steps)
| extend MessageData = parse_json(tostring(Message.data))
| extend MalwareData = parse_json(tostring(MessageData.message))
| extend
ClusterID = tostring(MessageData.clusterID),
WorkspaceID = tostring(MessageData.wsID),
DevSpaceId = tostring(MalwareData.dev_space_id),
User = tostring(MalwareData.user),
Malware = strcat_array(MalwareData.findings, ", "),
MessageText = tostring(MalwareData.message)
| extend
AccountName = tostring(split(User, '@')[0]),
UPNSuffix = tostring(split(User, '@')[1])
Stage 4: where
| where isnotempty(User) and isnotempty(Malware)
Stage 5: project
| project
UpdatedOn,
ClusterID,
WorkspaceID,
DevSpaceId,
MessageText,
User,
Malware,
Tenant,
SpaceId,
Category,
CloudApp = "SAP BTP",
AccountName,
UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Malware | is_not_null | field:"Malware" kind:is_not_null | |
Message | match |
| field:"Message" kind:match value:"malware" |
User | is_not_null | field:"user" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
Category | project |
CloudApp | project |
ClusterID | project |
DevSpaceId | project |
Malware | project |
MessageText | project |
SpaceId | project |
Tenant | project |
UPNSuffix | project |
UpdatedOn | project |
User | project |
WorkspaceID | project |
Tenable.ad DCShadow
#'Searches for DCShadow attacks.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 861044f3-6eef-4f79-8609-e3764abb02f4
name: Tenable.ad DCShadow
description: |
'Searches for DCShadow attacks.'
severity: High
requiredDataConnectors:
- connectorId: Tenable.ad
dataTypes:
- Tenable_ad_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1207
query: |
// For the query to work properly, make sure you have imported the afad_parser.kql parser into the workspace
// Retrieve the parser here: https://raw.githubusercontent.com/tenable/Azure-Sentinel/Tenable.ad-connector/Solutions/TenableAD/Parsers/afad_parser.kql
// Then, create the Kusto Function with alias afad_parser
afad_parser
| where MessageType == 2 and Codename == "DCShadow"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "DCShadow"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"DCShadow" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
TI Map Domain Entity to DeviceNetworkEvents
#This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceNetworkEvents action any: Network activity |
Rule Body
id: c308b2f3-eebe-4a20-905c-cb8293b062db
name: TI Map Domain Entity to DeviceNetworkEvents
description: |
'This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
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;
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteUrl)
| where TimeGenerated >= ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
| where isnotempty(Domain)
| project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
let DeviceNetworkEventDomains = DeviceNetworkEvents_
| distinct Domain
| summarize make_list(Domain);
ThreatIntelligenceIndicator
| where isnotempty(DomainName)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_Domain = tolower(DomainName)
| where TI_Domain in (DeviceNetworkEventDomains)
| 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;"
| join kind=innerunique (DeviceNetworkEvents_) on $left.TI_Domain == $right.Domain
| where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime
| summarize DeviceNetworkEvents_TimeGenerated = arg_max(DeviceNetworkEvents_TimeGenerated, *) by IndicatorId, TI_Domain
| project DeviceNetworkEvents_TimeGenerated, IndicatorId, TI_Domain, Url = RemoteUrl, ConfidenceScore, Description, ThreatType, Tags, TrafficLightProtocolLevel, ActionType, DeviceId, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort
| extend Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
| extend timestamp = DeviceNetworkEvents_TimeGenerated, UserPrincipalName = InitiatingProcessAccountUpn
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: InitiatingProcessCommandLine
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: DeviceNetworkEvents_
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteUrl)
| where TimeGenerated >= ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
| where isnotempty(Domain)
| project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
Let Binding: DeviceNetworkEventDomains
let DeviceNetworkEventDomains = DeviceNetworkEvents_
| distinct Domain
| summarize make_list(Domain);
Stage 1: source
let DeviceNetworkEvents_
Stage 2: source
let DeviceNetworkEventDomains
Stage 3: source
ThreatIntelligenceIndicator
Stage 4: where
where isnotempty(DomainName)
Stage 5: where
where TimeGenerated >= ago(1209600s)
Stage 6: extend
extend TI_Domain
Stage 7: where
where TI_Domain == "DeviceNetworkEventDomains"
Stage 8: summarize
summarize LatestIndicatorTime by IndicatorId
Stage 9: where
where Active == true
Stage 10: where
where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")
Stage 11: join
join kind=innerunique (DeviceNetworkEvents_) on $left.TI_Domain, $right.Domain
Stage 12: where
where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime
Stage 13: summarize
summarize DeviceNetworkEvents_TimeGenerated by IndicatorId, TI_Domain
Stage 14: project
project ActionType, ConfidenceScore, Description, DeviceId, DeviceName, DeviceNetworkEvents_TimeGenerated, IndicatorId, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort, TI_Domain, Tags, ThreatType, TrafficLightProtocolLevel, Url
Stage 15: extend
extend Name, UPNSuffix
Stage 16: extend
extend UserPrincipalName, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
Domain | is_not_null | field:"Domain" kind:is_not_null | |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
RemoteUrl | is_not_null | field:"RemoteUrl" kind:is_not_null | |
TI_Domain | in |
| field:"TI_Domain" kind:in value:"DeviceNetworkEventDomains" |
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;" |
ActionType | match | ConnectionFailed | excludes:ActionType field:"ActionType" value:"ConnectionFailed" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActionType | project |
ConfidenceScore | project |
Description | project |
DeviceId | project |
DeviceName | project |
DeviceNetworkEvents_TimeGenerated | project |
IndicatorId | project |
InitiatingProcessAccountUpn | project |
InitiatingProcessCommandLine | project |
RemoteIP | project |
RemotePort | project |
TI_Domain | project |
Tags | project |
ThreatType | project |
TrafficLightProtocolLevel | project |
Url | project |
Name | extend |
UPNSuffix | extend |
UserPrincipalName | 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: bc3bb047-70b8-4a4b-ac21-e3b1172881a4
name: TI Map Domain entity to EmailEvents
description: |
Identifies a match in EmailEvents table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- EmailEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
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(ThreatIntelIndicators
| where TimeGenerated >= ago(ioc_lookBack)
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend IndicatorId = tostring(split(Id, "--")[2])
| where DomainName in (SenderDomains) or DomainName in (RecipientDomains)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend Description = tostring(parse_json(Data).description)
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
(union
(TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.SenderFromDomain),
(TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.RecipientEmailDomain))
| where EmailEvents_TimeGenerated < ValidUntil
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Type, 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.5
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.DomainName == $right.SenderFromDomain
Leg 2: TI
TI | join kind=innerunique (EmailEvents_) on $left.DomainName == $right.RecipientEmailDomain
Applied to the Combined Result
| where EmailEvents_TimeGenerated < ValidUntil
| extend ActivityGroupNames = extract("ActivityGroup:([^,]+)", 1, tostring(parse_json(Data).labels))
| summarize EmailEvents_TimeGenerated = arg_max(EmailEvents_TimeGenerated, *) by Id, RecipientEmailAddress
| project EmailEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, DomainName, RecipientEmailAddress, SenderFromAddress, Subject, ConfidenceLevel, Type, 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 |
|---|---|---|---|
DomainName | in |
| field:"DomainName" kind:in |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
RecipientEmailAddress | is_not_null | field:"RecipientEmailAddress" kind:is_not_null | |
SenderFromAddress | is_not_null | field:"SenderFromAddress" kind:is_not_null | |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
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 |
Confidence | project |
ConfidenceLevel | project |
DeliveryAction | project |
DeliveryLocation | project |
Description | project |
DomainName | project |
EmailDirection | project |
EmailEvents_TimeGenerated | project |
Id | project |
RecipientEmailAddress | project |
SenderFromAddress | project |
Subject | project |
TrafficLightProtocolLevel | project |
Type | project |
ValidUntil | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
UniFi Site Manager: New WAN2 (secondary) issue recorded
#Identifies a new issue index on the secondary WAN failover link. Fires only on newly-recorded issue indexes, not while existing issues persist.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: b16f13ae-343b-9513-e684-469cdf9471b2
name: "UniFi Site Manager: New WAN2 (secondary) issue recorded"
description: |
Identifies a new issue index on the secondary WAN failover link. Fires only on newly-recorded issue indexes, not while existing issues persist.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Sites_CL
queryFrequency: 15m
queryPeriod: 45m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1498
subTechniques:
- T1498.001
query: |
// UniFi reports a monotonically-rising wanIssues.count rather than discrete events.
// Detect a NEW occurrence by checking whether the per-site max(count) has grown
// in the most recent 15-min window vs the prior 15-min window.
Unifi_SiteManager_Sites_CL
| where TimeGenerated > ago(45m)
| extend siteId_s = tostring(SiteId)
| where isnotempty(tostring(SiteStatistics.wans.WAN2))
| mv-expand issue = todynamic(tostring(SiteStatistics.wans.WAN2.wanIssues))
| extend issueCount = tolong(issue.count), idx = tolong(issue.index)
| summarize
PrevMaxCount = maxif(issueCount, TimeGenerated < ago(15m)),
CurrentMaxCount = maxif(issueCount, TimeGenerated >= ago(15m)),
LatestIndex = maxif(idx, TimeGenerated >= ago(15m))
by siteId_s, SiteName
| extend PrevMaxCount = coalesce(PrevMaxCount, tolong(0))
| where CurrentMaxCount > PrevMaxCount
| extend
TimeGenerated = now(),
Delta = CurrentMaxCount - PrevMaxCount,
Activity = strcat('New WAN2 issue: count rose from ', PrevMaxCount, ' to ', CurrentMaxCount)
| project TimeGenerated, SiteId = siteId_s, SiteName, Activity, IssueIndex = LatestIndex, PrevMaxCount, CurrentMaxCount, Delta
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SiteId
- identifier: DnsDomain
columnName: SiteName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
Unifi_SiteManager_Sites_CL
Stage 2: where
| where TimeGenerated > ago(45m)
Stage 3: extend
| extend siteId_s = tostring(SiteId)
Stage 4: where
| where isnotempty(tostring(SiteStatistics.wans.WAN2))
Stage 5: mv-expand
| mv-expand issue = todynamic(tostring(SiteStatistics.wans.WAN2.wanIssues))
Stage 6: extend
| extend issueCount = tolong(issue.count), idx = tolong(issue.index)
Stage 7: summarize
| summarize
PrevMaxCount = maxif(issueCount, TimeGenerated < ago(15m)),
CurrentMaxCount = maxif(issueCount, TimeGenerated >= ago(15m)),
LatestIndex = maxif(idx, TimeGenerated >= ago(15m))
by siteId_s, SiteName
Stage 8: extend
| extend PrevMaxCount = coalesce(PrevMaxCount, tolong(0))
Stage 9: where
| where CurrentMaxCount > PrevMaxCount
Stage 10: extend
| extend
TimeGenerated = now(),
Delta = CurrentMaxCount - PrevMaxCount,
Activity = strcat('New WAN2 issue: count rose from ', PrevMaxCount, ' to ', CurrentMaxCount)
Stage 11: project
| project TimeGenerated, SiteId = siteId_s, SiteName, Activity, IssueIndex = LatestIndex, PrevMaxCount, CurrentMaxCount, Delta
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SiteStatistics.wans.WAN2 | is_not_null | field:"SiteStatistics.wans.WAN2" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
CurrentMaxCount | project |
Delta | project |
IssueIndex | project |
PrevMaxCount | project |
SiteId | project |
SiteName | project |
TimeGenerated | project |
User Added to Admin Role
#Detects a user being added to a new privileged role. Monitor these additions to ensure the users are made eligible for these roles are intended to have these levels of access. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add eligible member (eligible) |
| Azure | Add eligible member (permanent) |
| Azure | Add member to role |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
Rule Body
id: 2a09f8cb-deb7-4c40-b08b-9137667f1c0b
name: User Added to Admin Role
description: |
'Detects a user being added to a new privileged role. Monitor these additions to ensure the users are made eligible for these roles are intended to have these levels of access.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
AuditLogs
| where OperationName in ("Add eligible member (permanent)", "Add eligible member (eligible)", "Add member to role")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend Target = tostring(TargetResource.userPrincipalName),
props = TargetResource.modifiedProperties
)
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"',tostring(Property.newValue))
)
| where RoleName contains "admin"
| 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 = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
| extend TargetUserPrincipalName = iff(OperationName=="Add member to role",tostring(TargetResources[0].userPrincipalName),tostring(TargetResources[2].userPrincipalName))
| extend TargetAadUserId = iff(OperationName=="Add member to role", tostring(TargetResources[0].id), tostring(TargetResources[2].id))
| extend AddedUser = TargetUserPrincipalName
| extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| project-reorder TimeGenerated, AddedUser, RoleName, InitiatedBy
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetAccountName
- identifier: UPNSuffix
columnName: TargetAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: TargetAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName in ("Add eligible member (permanent)", "Add eligible member (eligible)", "Add member to role")
Stage 3: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend Target = tostring(TargetResource.userPrincipalName),
props = TargetResource.modifiedProperties
)
Stage 4: kusto:mv-apply
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"',tostring(Property.newValue))
)
Stage 5: where
| where RoleName contains "admin"
Stage 6: extend (11 consecutive steps)
| 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 = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
| extend TargetUserPrincipalName = iff(OperationName=="Add member to role",tostring(TargetResources[0].userPrincipalName),tostring(TargetResources[2].userPrincipalName))
| extend TargetAadUserId = iff(OperationName=="Add member to role", tostring(TargetResources[0].id), tostring(TargetResources[2].id))
| extend AddedUser = TargetUserPrincipalName
| extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Stage 7: project-reorder
| project-reorder TimeGenerated, AddedUser, RoleName, InitiatedBy
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | in |
| field:"OperationName" kind:in |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"Role.DisplayName" |
RoleName | contains |
| field:"RoleName" kind:contains value:"admin" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"User" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIPAddress | extend |
InitiatedBy | extend |
TargetUserPrincipalName | extend |
TargetAadUserId | extend |
AddedUser | extend |
TargetAccountName | extend |
TargetAccountUPNSuffix | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
ZeroFox Alerts - Informational Severity Alerts
#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.
'Detects informational severity alerts from ZeroFox'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Initial Access |
Rule Body
id: 6f7a7413-b72f-4361-84ee-897baeb9c6d4
name: ZeroFox Alerts - Informational Severity Alerts
description: |
'Detects informational severity alerts from ZeroFox'
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: ZeroFox_Alert_Polling
dataTypes:
- ZeroFoxAlertPoller_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- ResourceDevelopment
- InitialAccess
relevantTechniques:
- T1583
- T1586
- T1566
query: |
ZeroFoxAlertPoller_CL
| where Severity in (1,2)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: entity_name_s
eventGroupingSettings:
aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
ZeroFoxAlertPoller_CL
Stage 2: where
| where Severity in (1,2)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | in |
| field:"Severity" kind:in |
Zscaler - Unexpected update operation
#'Detects unexpected version of update operation.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 672e2846-4226-11ec-81d3-0242ac130003
name: Zscaler - Unexpected update operation
description: |
'Detects unexpected version of update operation.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
datatypes:
- ZPA_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
ZPAEvent
| where isnotempty(AuditOperationType)
| extend ExpectedVersion1 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditOldValue)
| extend ExpectedVersion2 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditOldValue)
| extend NewVersion1 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditNewValue)
| extend NewVersion2 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditNewValue)
| extend Comparision1 = iff(tolong(ExpectedVersion2) > tolong(NewVersion2), 'Unexpected version', 'Expected version')
| extend VersionCheck = iff(tolong(ExpectedVersion1) > tolong(NewVersion1), 'Unexpected version', Comparision1)
| project-away ExpectedVersion1, ExpectedVersion2, NewVersion1, NewVersion2, Comparision1
| extend ProcessCustomEntity = AuditOperationType
entityMappings:
- entityType: Process
fieldMappings:
- identifier: ProcessId
columnName: ProcessCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
ZPAEvent
Stage 2: where
| where isnotempty(AuditOperationType)
Stage 3: extend (6 consecutive steps)
| extend ExpectedVersion1 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditOldValue)
| extend ExpectedVersion2 = extract(@'(expectedVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditOldValue)
| extend NewVersion1 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 2, AuditNewValue)
| extend NewVersion2 = extract(@'("currentVersion":")([0-9]+)\.([0-9]+\.[0-9]+)', 3, AuditNewValue)
| extend Comparision1 = iff(tolong(ExpectedVersion2) > tolong(NewVersion2), 'Unexpected version', 'Expected version')
| extend VersionCheck = iff(tolong(ExpectedVersion1) > tolong(NewVersion1), 'Unexpected version', Comparision1)
Stage 4: project-away
| project-away ExpectedVersion1, ExpectedVersion2, NewVersion1, NewVersion2, Comparision1
Stage 5: extend
| extend ProcessCustomEntity = AuditOperationType
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AuditOperationType | is_not_null | field:"AuditOperationType" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
VersionCheck | extend |
ProcessCustomEntity | extend |