Detection rules › Kusto
Kusto rules: 11
Abnormal Port to Protocol
#'Identifies communication for well known protocol over a non-standard port based on learning period activity. This can indicate malicious communication (C2) or exfiltration by attackers trying to communicate over known ports (22:SSH, 80:HTTP) but dont use the known protocol headers to match the port number. Configurable Parameters: - Learning period time - learning period for protocol learning in days. Default is set to 7.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
Rule Body
id: 826f930c-2f25-4508-8e75-a95b809a4e15
name: Abnormal Port to Protocol
description: |
'Identifies communication for well known protocol over a non-standard port based on learning period activity. This can indicate malicious communication (C2) or exfiltration by attackers trying to communicate over known ports (22:SSH, 80:HTTP) but dont use the known protocol headers to match the port number.
Configurable Parameters:
- Learning period time - learning period for protocol learning in days. Default is set to 7.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- AZFWApplicationRule
- AZFWNetworkRule
queryFrequency: 1h
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- CommandAndControl
- DefenseEvasion
relevantTechniques:
- T1041
- T1571
- T1572
query: |
let LearningPeriod = 7d;
let RunTime = 1h;
let StartLearningPeriod = LearningPeriod + RunTime;
let DetectionWindowStart = ago(RunTime);
let LearningPortToProtocol1 = (AzureDiagnostics
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where OperationName == "AzureFirewallApplicationRuleLog"
| extend msg_s= column_ifexists('msg_s',Message)
| parse msg_s with Protocol " request from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int "." *
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
let LearningPortToProtocol2 = (AZFWNetworkRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| extend Fqdn = DestinationIp
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
let LearningPortToProtocol3 = (AZFWApplicationRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
let AlertTimePortToProtocol1 = (AzureDiagnostics
| where TimeGenerated between (DetectionWindowStart .. now())
| where OperationName == "AzureFirewallApplicationRuleLog"
| extend msg_s= column_ifexists('msg_s',Message)
| parse msg_s with Protocol " request from " SourceIp ":" SourcePort " to " Fqdn ":" DestinationPort:int "." *
| where isnotempty(DestinationPort)
| summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
let AlertTimePortToProtocol2 = (AZFWNetworkRule
| where TimeGenerated between (DetectionWindowStart .. now())
| where isnotempty(DestinationPort)
| extend Fqdn = DestinationIp
| summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
let AlertTimePortToProtocol3 = (AZFWApplicationRule
| where TimeGenerated between (DetectionWindowStart .. now())
| where isnotempty(DestinationPort)
| summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
(union isfuzzy=true
(AlertTimePortToProtocol1
| join kind=leftouter (LearningPortToProtocol1) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol),
(AlertTimePortToProtocol2
| join kind=leftanti (LearningPortToProtocol2) on $left.AlertTimeDstPort == $right.LearningTimeDstPort, $left.AlertTimeProtocol == $right.LearningTimeProtocol),
(AlertTimePortToProtocol3
| join kind=leftouter (LearningPortToProtocol3) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol))
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Fqdn
customDetails:
AbnormalPort: AlertTimeDstPort
NetworkProtocol: AlertTimeProtocol
alertDetailsOverride:
alertDisplayNameFormat: 'Abnormal Port to Protocol Communication Detected from {{SourceIp}} to {{Fqdn}}'
alertDescriptionFormat: 'Communication was observed over port {{AlertTimeDstPort}} using protocol {{AlertTimeProtocol}}, which is different from the protocol observed during the learning period. This could indicate potential malicious activity such as C2 communication or data exfiltration.'
version: 1.1.5
kind: Scheduled
Stages and Predicates
Parameters
let LearningPeriod = 7d;
let RunTime = 1h;
let StartLearningPeriod = LearningPeriod + RunTime;
let DetectionWindowStart = ago(RunTime);
Let Binding: LearningPortToProtocol1
let LearningPortToProtocol1 = (AzureDiagnostics
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where OperationName == "AzureFirewallApplicationRuleLog"
| extend msg_s= column_ifexists('msg_s',Message)
| parse msg_s with Protocol " request from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int "." *
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
Let Binding: LearningPortToProtocol2
let LearningPortToProtocol2 = (AZFWNetworkRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| extend Fqdn = DestinationIp
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
Let Binding: LearningPortToProtocol3
let LearningPortToProtocol3 = (AZFWApplicationRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
Union isfuzzy=true (3 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: AlertTimePortToProtocol1, AlertTimePortToProtocol2, AlertTimePortToProtocol3
Leg 1: AlertTimePortToProtocol1
AlertTimePortToProtocol1
| join kind=leftouter (LearningPortToProtocol1) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol
Leg 2: AlertTimePortToProtocol2
AlertTimePortToProtocol2
| join kind=leftanti (LearningPortToProtocol2) on $left.AlertTimeDstPort == $right.LearningTimeDstPort, $left.AlertTimeProtocol == $right.LearningTimeProtocol
Leg 3: AlertTimePortToProtocol3
AlertTimePortToProtocol3
| join kind=leftouter (LearningPortToProtocol3) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationPort | is_not_null | field:"DestinationPort" kind:is_not_null | |
OperationName | eq |
| field:"OperationName" kind:eq value:"AzureFirewallApplicationRuleLog" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DestinationPort | is_not_null | excludes:DestinationPort |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Fqdn | summarize |
LearningTimeDstPort | summarize |
LearningTimeProtocol | summarize |
SourceIp | summarize |
Cisco Umbrella - Windows PowerShell User-Agent Detected
#'Rule helps to detect Powershell user-agent activity by an unusual process other than a web browser.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | No specific technique |
| Command & Control | No specific technique |
Rule Body
id: b12b3dab-d973-45af-b07e-e29bb34d8db9
name: Cisco Umbrella - Windows PowerShell User-Agent Detected
description: |
'Rule helps to detect Powershell user-agent activity by an unusual process other than a web browser.'
severity: Medium
requiredDataConnectors:
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_proxy_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- DefenseEvasion
query: |
let timeframe = 15m;
Cisco_Umbrella
| where EventType == "proxylogs"
| where TimeGenerated > ago(timeframe)
| where HttpUserAgentOriginal contains "WindowsPowerShell"
| extend Message = "Windows PowerShell User Agent"
| project Message, SrcIpAddr, DstIpAddr, UrlOriginal, TimeGenerated,HttpUserAgentOriginal
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlOriginal
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 15m;
Stage 1: source
Cisco_Umbrella
Stage 2: where
| where EventType == "proxylogs"
Stage 3: where
| where TimeGenerated > ago(timeframe)
Stage 4: where
| where HttpUserAgentOriginal contains "WindowsPowerShell"
Stage 5: extend
| extend Message = "Windows PowerShell User Agent"
Stage 6: project
| project Message, SrcIpAddr, DstIpAddr, UrlOriginal, TimeGenerated,HttpUserAgentOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"proxylogs" |
HttpUserAgentOriginal | contains |
| field:"HttpUserAgentOriginal" kind:contains value:"WindowsPowerShell" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstIpAddr | project |
HttpUserAgentOriginal | project |
Message | project |
SrcIpAddr | project |
TimeGenerated | project |
UrlOriginal | project |
Claroty - Threat detected
#Detects Claroty events where EventOriginalType or EventType contains 'Threat', identifying Threat-related activity and surfacing the destination IP address for investigation.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Discovery |
Rule Body
id: 731e5ac4-7fe1-4b06-9941-532f2e008bb3
name: Claroty - Threat detected
description: Detects Claroty events where EventOriginalType or EventType contains 'Threat', identifying Threat-related activity
and surfacing the destination IP address for investigation.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Claroty
dataTypes:
- ClarotyEvent
- connectorId: ClarotyAma
dataTypes:
- ClarotyEvent
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- Reconnaissance
relevantTechniques:
- T1018
- T1595
query: |
ClarotyEvent
| where EventOriginalType has 'Threat' or EventType has 'Threat'
| extend IPCustomEntity = DstIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
alertDetailsOverride:
alertDisplayNameFormat: Claroty Threat detected on {{IPCustomEntity}}
alertDescriptionFormat: Claroty event type {{EventType}} or original type {{EventOriginalType}} matched Threat on {{IPCustomEntity}}
customDetails:
EventOriginalType: EventOriginalType
EventType: EventType
TimeGenerated: TimeGenerated
version: 1.0.5
kind: Scheduled
Stages and Predicates
Stage 1: source
ClarotyEvent
Stage 2: where
| where EventOriginalType has 'Threat' or EventType has 'Threat'
Stage 3: extend
| extend IPCustomEntity = DstIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventOriginalType | match |
| field:"EventOriginalType" kind:match value:"Threat" |
EventType | match |
| field:"EventType" kind:match value:"Threat" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
Cloudflare - Client request from country in blocklist
#'Detects requests from countries which are in blocklist.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 40554544-6e4a-4413-8d14-bf2de939c5d0
name: Cloudflare - Client request from country in blocklist
description: |
'Detects requests from countries which are in blocklist.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let bl_countries = dynamic(['cn', 'hk']);
Cloudflare
| where SrcGeoCountry in~ (bl_countries)
| 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
Parameters
let bl_countries = dynamic(['cn', 'hk']);
Stage 1: source
Cloudflare
Stage 2: where
| where SrcGeoCountry in~ (bl_countries)
Stage 3: extend
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SrcGeoCountry | in |
| field:"SrcGeoCountry" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CompleteUrl | extend |
CYFIRMA - Brand Intelligence - Malicious Mobile App High Rule
#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 analytic rule detects instances where malicious or unauthorized mobile applications are discovered mimicking legitimate brand assets. Such impersonations may be distributed through unofficial app stores or third-party websites, potentially deceiving customers, harvesting sensitive data, or damaging brand reputation. This alert is triggered when CYFIRMA threat intelligence identifies a suspicious mobile app associated with the organization's brand or product names."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Execution | |
| Defense Evasion | |
| Credential Access | |
| Collection | |
| Command and Control |
Rule Body
id: 3176ac89-b195-48b7-a01e-740a6b26fb2f
name: CYFIRMA - Brand Intelligence - Malicious Mobile App High Rule
description: |
"This analytic rule detects instances where malicious or unauthorized mobile applications are discovered mimicking legitimate brand assets.
Such impersonations may be distributed through unofficial app stores or third-party websites, potentially deceiving customers, harvesting sensitive data, or damaging brand reputation.
This alert is triggered when CYFIRMA threat intelligence identifies a suspicious mobile app associated with the organization's brand or product names."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaBrandIntelligenceAlertsDC
dataTypes:
- CyfirmaBIMaliciousMobileAppsAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- ResourceDevelopment
- Execution
- DefenseEvasion
- CredentialAccess
- CommandAndControl
relevantTechniques:
- T1406
- T1414
- T1437
- T1583.001
- T1204.002
query: |
// High severity - Malicious Mobile App Impersonation
let timeFrame = 5m;
CyfirmaBIMaliciousMobileAppsAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=asset_value,
Impact=impact,
Recommendation=recommendation,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
Recommendation,
ProductName,
ProviderName
customDetails:
AssetType: AssetType
AssetValue: AssetValue
Impact: Impact
Recommendation: Recommendation
Description: Description
RiskScore: RiskScore
TimeGenerated: TimeGenerated
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Alert: Malicious Mobile App Impersonating Brand Detected - {{AssetType}} - {{AssetValue}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaBIMaliciousMobileAppsAlerts_CL
Stage 2: where
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=asset_value,
Impact=impact,
Recommendation=recommendation,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
Recommendation,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
Recommendation | project |
RiskScore | project |
TimeGenerated | project |
UID | project |
Front Door Premium WAF - XSS Detection
#'Identifies a match for an XSS attack in the Front Door Premium WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements. References: https://owasp.org/www-project-top-ten/2017/A7_2017-Cross-Site_Scripting_(XSS)'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Execution |
Rules Detecting the Same Action
These rules filter on the same operation.
- AFD WAF - Code Injection (Kusto)
- AFD WAF - Path Traversal Attack (Kusto)
- Front Door Premium WAF - SQLi Detection (Kusto)
Rule Body
id: b7643904-5081-4920-917e-a559ddc3448f
name: Front Door Premium WAF - XSS Detection
description: |
'Identifies a match for an XSS attack in the Front Door Premium WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements.
References: https://owasp.org/www-project-top-ten/2017/A7_2017-Cross-Site_Scripting_(XSS)'
severity: High
status: Available
requiredDataConnectors:
- connectorId: WAF
dataTypes:
- AzureDiagnostics
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
relevantTechniques:
- T1189
- T1203
- T0853
tags:
- Cross Site Scripting
query: |
let Threshold = 1;
AzureDiagnostics
| where Category =~ "FrontDoorWebApplicationFirewallLog"
| where action_s =~ "AnomalyScoring"
| where details_msg_s has "XSS"
| parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *
| project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData
| join kind = inner(
AzureDiagnostics
| where Category =~ "FrontDoorWebApplicationFirewallLog"
| where action_s =~ "Block") on trackingReference_s
| summarize URI_s = make_set(requestUri_s,100), Table = make_set(TableName,100), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), TrackingReference = make_set(trackingReference_s,100), Matched_Data = make_set(MatchedData,100), Detail_Data = make_set(details_data_s,100), Detail_Message = make_set(details_msg_s,100), Total_TrackingReference = dcount(trackingReference_s) by clientIP_s, host_s, action_s
| where Total_TrackingReference >= Threshold
# The Threshold value above can be changed as per your infrastructure's requirement
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URI_s
- entityType: IP
fieldMappings:
- identifier: Address
columnName: clientIP_s
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let Threshold = 1;
Stage 1: source
AzureDiagnostics
Stage 2: where
| where Category =~ "FrontDoorWebApplicationFirewallLog"
Stage 3: where
| where action_s =~ "AnomalyScoring"
Stage 4: where
| where details_msg_s has "XSS"
Stage 5: parse
| parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *
Stage 6: project
| project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData
Stage 7: join
| join kind = inner(
AzureDiagnostics
| where Category =~ "FrontDoorWebApplicationFirewallLog"
| where action_s =~ "Block") on trackingReference_s
Stage 8: summarize
summarize Detail_Data, Detail_Message, EndTime, Matched_Data, StartTime, Table, Total_TrackingReference, TrackingReference, URI_s by clientIP_s, host_s, action_s
Stage 9: where
where Total_TrackingReference >= 1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"FrontDoorWebApplicationFirewallLog" |
Total_TrackingReference | ge |
| field:"Total_TrackingReference" kind:ge value:"1" |
action_s | eq |
| field:"action_s" kind:eq |
details_msg_s | match |
| field:"details_msg_s" kind:match value:"XSS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Detail_Data | summarize |
Detail_Message | summarize |
EndTime | summarize |
Matched_Data | summarize |
StartTime | summarize |
Table | summarize |
Total_TrackingReference | summarize |
TrackingReference | summarize |
URI_s | summarize |
action_s | summarize |
clientIP_s | summarize |
host_s | summarize |
GitLab - Personal Access Tokens creation over time
#'This queries GitLab Audit Logs for access tokens. Attacker can exfiltrate data from you GitLab repository after gaining access to it by generating or hijacking access tokens. This hunting queries allows you to track the personal access tokens creation for each of your repositories. The visualization allow you to quickly identify anomalies/excessive creation, to further investigate repo access & permissions.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Rule Body
id: 4d6d8b0e-6d9a-4857-a141-f5d89393cddb
name: GitLab - Personal Access Tokens creation over time
description: |
'This queries GitLab Audit Logs for access tokens. Attacker can exfiltrate data from you GitLab repository after gaining access to it by generating or hijacking access tokens.
This hunting queries allows you to track the personal access tokens creation for each of your repositories.
The visualization allow you to quickly identify anomalies/excessive creation, to further investigate repo access & permissions.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1213
query: |
// l_min_tokens_created - minimum tokens created per repository per user per day to consider
let l_min_tokens_created = 0;
let interval = 1d;
GitLabAudit
| where TargetType == "PersonalAccessToken"
| project Severity, TimeGenerated = bin(todatetime(EventTime),1d), AuthorName, IPAddress, Repository = EntityName, Action, TargetType
| summarize total = count() by Repository, TimeGenerated, AuthorName
| where total >= l_min_tokens_created
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AuthorName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let l_min_tokens_created = 0;
let interval = 1d;
Stage 1: source
GitLabAudit
Stage 2: where
| where TargetType == "PersonalAccessToken"
Stage 3: project
| project Severity, TimeGenerated = bin(todatetime(EventTime),1d), AuthorName, IPAddress, Repository = EntityName, Action, TargetType
Stage 4: summarize
| summarize total = count() by Repository, TimeGenerated, AuthorName
Stage 5: where
| where total >= l_min_tokens_created
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
TargetType | eq |
| field:"TargetType" kind:eq value:"PersonalAccessToken" |
total | ge |
| field:"total" kind:ge value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AuthorName | summarize |
Repository | summarize |
TimeGenerated | summarize |
total | summarize |
Idira - High-Risk Actions Outside Business Hours
#Detects privileged or destructive actions (delete/disable/rotate/elevate/etc.) occurring outside standard business hours. Useful for insider misuse or compromised admin detection.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | No specific technique |
Rule Body
id: 3733e1ac-991b-4504-99e8-24ff4fbaf6e4
kind: Scheduled
name: Idira - High-Risk Actions Outside Business Hours
description: >
Detects privileged or destructive actions (delete/disable/rotate/elevate/etc.)
occurring outside standard business hours. Useful for insider misuse or
compromised admin detection.
severity: High
tactics:
- DefenseEvasion
queryFrequency: 10M
queryPeriod: 1D
triggerOperator: GreaterThan
triggerThreshold: 0
query: |
let risky = dynamic(["Delete","Remove","Rotate","Elevate","Disable","Grant","Policy","Safe","Vault","Key"]);
CyberArk_AuditEvents_CL
| where isnotempty(action) or isnotempty(actionType) or isnotempty(auditType) or isnotempty(message)
| where hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) > 20
| where action has_any (risky) or actionType has_any (risky) or auditType has_any (risky) or message has_any (risky)
| extend cd = parse_json(customData)
| extend cd_username = tostring(cd.username),
cd_auth_method = tostring(cd.authentication_method),
cd_client_ip = tostring(cd.client_ip_address),
cd_source_ip = tostring(cd.source_ip_address),
cd_device_os = tostring(cd.device_os),
cd_browser = tostring(cd.browser_name),
cd_geo_city = tostring(cd.geoip_city_name),
cd_geo_country = tostring(cd.geoip_country_name),
cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
| project TimeGenerated, CyberArkTenantId, username, cd_username, identityType, action, actionType, auditType,
target, targetAccount, safe, source, cd_source_ip, cd_client_ip, cd_auth_method, cd_device_os,
cd_browser, cd_geo_city, cd_geo_country, cd_target, component, serviceName, message
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: username
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: target
- entityType: IP
fieldMappings:
- identifier: Address
columnName: source
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Parameters
let risky = dynamic(["Delete","Remove","Rotate","Elevate","Disable","Grant","Policy","Safe","Vault","Key"]);
Stage 1: source
CyberArk_AuditEvents_CL
Stage 2: where
| where isnotempty(action) or isnotempty(actionType) or isnotempty(auditType) or isnotempty(message)
Stage 3: where
| where hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) > 20
Stage 4: where
| where action has_any (risky) or actionType has_any (risky) or auditType has_any (risky) or message has_any (risky)
Stage 5: extend
| extend cd = parse_json(customData)
Stage 6: extend
| extend cd_username = tostring(cd.username),
cd_auth_method = tostring(cd.authentication_method),
cd_client_ip = tostring(cd.client_ip_address),
cd_source_ip = tostring(cd.source_ip_address),
cd_device_os = tostring(cd.device_os),
cd_browser = tostring(cd.browser_name),
cd_geo_city = tostring(cd.geoip_city_name),
cd_geo_country = tostring(cd.geoip_country_name),
cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
Stage 7: project
| project TimeGenerated, CyberArkTenantId, username, cd_username, identityType, action, actionType, auditType,
target, targetAccount, safe, source, cd_source_ip, cd_client_ip, cd_auth_method, cd_device_os,
cd_browser, cd_geo_city, cd_geo_country, cd_target, component, serviceName, message
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
TimeGenerated | gt |
| field:"TimeGenerated" kind:gt value:"20" |
TimeGenerated | lt |
| field:"TimeGenerated" kind:lt value:"7" |
action | is_not_null | field:"action" kind:is_not_null | |
action | match |
| field:"action" kind:match |
actionType | is_not_null | field:"actionType" kind:is_not_null | |
actionType | match |
| field:"actionType" kind:match |
auditType | is_not_null | field:"auditType" kind:is_not_null | |
auditType | match |
| field:"auditType" kind:match |
message | is_not_null | field:"message" kind:is_not_null | |
message | match |
| field:"message" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CyberArkTenantId | project |
TimeGenerated | project |
action | project |
actionType | project |
auditType | project |
cd_auth_method | project |
cd_browser | project |
cd_client_ip | project |
cd_device_os | project |
cd_geo_city | project |
cd_geo_country | project |
cd_source_ip | project |
cd_target | project |
cd_username | project |
component | project |
identityType | project |
message | project |
safe | project |
serviceName | project |
source | project |
target | project |
targetAccount | project |
username | project |
Infoblox - Many NXDOMAIN DNS Responses Detected
#'Detected at least 200 DNS responses for non-existent domains in 1 hour generated by single host. Queries do not need to be the same. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxCDC.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: b2f34315-9065-488e-88d0-a171d2b0da8e
name: Infoblox - Many NXDOMAIN DNS Responses Detected
description: |
'Detected at least 200 DNS responses for non-existent domains in 1 hour generated by single host. Queries do not need to be the same. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxCDC**](https://aka.ms/sentinel-InfobloxCloudDataConnector-parser).'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1498
- T1565
query: |
let threshold = 200;
InfobloxCDC
| where DeviceEventClassID == "DNS Response"
| where InfobloxDNSRCode == "NXDOMAIN"
| summarize count() by SourceIP
| where count_ > threshold
| join kind=inner (InfobloxCDC
| where DeviceEventClassID == "DNS Response"
| where InfobloxDNSRCode == "NXDOMAIN"
) on SourceIP
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
- identifier: OSVersion
columnName: InfobloxB1SrcOSVersion
- identifier: FullName
columnName: SourceUserName
customDetails:
SourceMACAddress: SourceMACAddress
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 200;
Stage 1: source
InfobloxCDC
Stage 2: where
| where DeviceEventClassID == "DNS Response"
Stage 3: where
| where InfobloxDNSRCode == "NXDOMAIN"
Stage 4: summarize
| summarize count() by SourceIP
Stage 5: where
| where count_ > threshold
Stage 6: join
| join kind=inner (InfobloxCDC
| where DeviceEventClassID == "DNS Response"
| where InfobloxDNSRCode == "NXDOMAIN"
) on SourceIP
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"DNS Response" |
InfobloxDNSRCode | eq |
| field:"InfobloxDNSRCode" kind:eq value:"NXDOMAIN" |
count_ | gt |
| field:"count_" kind:gt value:"200" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SourceIP | summarize |
Lumen TI IPAddress in CommonSecurityLog
#This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: bc8a262a-5db3-4ac1-8757-519ed36ed929
name: Lumen TI IPAddress in CommonSecurityLog
displayName: Lumen TI IPAddress in CommonSecurityLog
description: |
This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.
severity: Medium
requiredDataConnectors:
- connectorId: LumenThreatFeedConnector
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceUploadIndicatorsAPI
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: CEF
dataTypes:
- CommonSecurityLog
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
let dt_lookBack = 1h; // Look back 1 hour for CommonSecurityLog events
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to IP addresses
let IP_Indicators = ThreatIntelIndicators
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
| where IsActive == true and ValidUntil > now()
| where SourceSystem == 'Lumen'
| where ObservableKey == "ipv4-addr:value"
| extend TI_ipEntity = ObservableValue
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127.";
// Perform a join between IP indicators and CommonSecurityLog events
IP_Indicators
| join kind=innerunique (
CommonSecurityLog
| where TimeGenerated >= ago(dt_lookBack)
| extend MessageIP = extract(IPRegex, 0, Message)
| extend CS_ipEntity = iff(isnotempty(SourceIP), SourceIP, DestinationIP)
| extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CS_ipEntity
| where CommonSecurityLog_TimeGenerated < ValidUntil
| summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id, CS_ipEntity
| project timestamp = CommonSecurityLog_TimeGenerated, SourceIP, DestinationIP, MessageIP, Message, DeviceVendor, DeviceProduct, Id, Tags, ValidUntil, Confidence, TI_ipEntity, CS_ipEntity, LogSeverity, DeviceAction, Type
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CS_ipEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let IP_Indicators is inlined into the numbered stages below.
Stages 1 to 8 define let IP_Indicators as the rule's main pipeline source. Stages 9 to 12 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 3: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
Stage 4: where
| where IsActive == true and ValidUntil > now()
Stage 5: where
| where SourceSystem == 'Lumen'
Stage 6: where
| where ObservableKey == "ipv4-addr:value"
Stage 7: extend
| extend TI_ipEntity = ObservableValue
Stage 8: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 9: join
IP_Indicators
| join kind=innerunique (
CommonSecurityLog
| where TimeGenerated >= ago(dt_lookBack)
| extend MessageIP = extract(IPRegex, 0, Message)
| extend CS_ipEntity = iff(isnotempty(SourceIP), SourceIP, DestinationIP)
| extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.CS_ipEntity
Stage 10: where
where CommonSecurityLog_TimeGenerated < ValidUntil
Stage 11: summarize
summarize CommonSecurityLog_TimeGenerated by Id, CS_ipEntity
Stage 12: project
project CS_ipEntity, Confidence, DestinationIP, DeviceAction, DeviceProduct, DeviceVendor, Id, LogSeverity, Message, MessageIP, SourceIP, TI_ipEntity, Tags, Type, ValidUntil, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | eq |
| field:"ObservableKey" kind:eq value:"ipv4-addr:value" |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"Lumen" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CS_ipEntity | project |
Confidence | project |
DestinationIP | project |
DeviceAction | project |
DeviceProduct | project |
DeviceVendor | project |
Id | project |
LogSeverity | project |
Message | project |
MessageIP | project |
SourceIP | project |
TI_ipEntity | project |
Tags | project |
Type | project |
ValidUntil | project |
timestamp | project |
Malicious Inbox Rule
#Often times after the initial compromise the attackers create inbox rules to delete emails that contain certain keywords. This is done so as to limit ability to warn compromised users that they've been compromised. Below is a sample query that tries to detect this. Reference: https://www.reddit.com/r/sysadmin/comments/7kyp0a/recent_phishing_attempts_my_experience_and_what/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | Exchange audit operation New-InboxRule: Federated domain added |
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: 7b907bf7-77d4-41d0-a208-5643ff75bf9a
name: Malicious Inbox Rule
description: |
'Often times after the initial compromise the attackers create inbox rules to delete emails that contain certain keywords.
This is done so as to limit ability to warn compromised users that they've been compromised. Below is a sample query that tries to detect this.
Reference: https://www.reddit.com/r/sysadmin/comments/7kyp0a/recent_phishing_attempts_my_experience_and_what/'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- DefenseEvasion
relevantTechniques:
- T1098
- T1078
query: |
let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);
OfficeActivity
| where OfficeWorkload =~ "Exchange"
| where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded")
| where Parameters has "Deleted Items" or Parameters has "Junk Email" or Parameters has "DeleteMessage"
| extend Events=todynamic(Parameters)
| parse Events with * "SubjectContainsWords" SubjectContainsWords '}'*
| parse Events with * "BodyContainsWords" BodyContainsWords '}'*
| parse Events with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
| where SubjectContainsWords has_any (Keywords)
or BodyContainsWords has_any (Keywords)
or SubjectOrBodyContainsWords has_any (Keywords)
| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
| extend OriginatingServerName = tostring(split(OriginatingServer, " ")[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: OriginatingServerName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIPAddress
version: 2.0.4
kind: Scheduled
Stages and Predicates
Parameters
let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);
Stage 1: source
OfficeActivity
Stage 2: where
| where OfficeWorkload =~ "Exchange"
Stage 3: where
| where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded")
Stage 4: where
| where Parameters has "Deleted Items" or Parameters has "Junk Email" or Parameters has "DeleteMessage"
Stage 5: extend
| extend Events=todynamic(Parameters)
Stage 6: parse
| parse Events with * "SubjectContainsWords" SubjectContainsWords '}'*
Stage 7: parse
| parse Events with * "BodyContainsWords" BodyContainsWords '}'*
Stage 8: parse
| parse Events with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
Stage 9: where
| where SubjectContainsWords has_any (Keywords)
or BodyContainsWords has_any (Keywords)
or SubjectOrBodyContainsWords has_any (Keywords)
Stage 10: extend (3 consecutive steps)
| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
ClientIPAddress =ClientIP has "."tostring(split(ClientIP, ":")[0])ClientIP has "["tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0])))ClientIPStage 11: summarize
| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
Stage 12: extend
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
Stage 13: extend
| extend OriginatingServerName = tostring(split(OriginatingServer, " ")[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
BodyContainsWords | match |
| field:"BodyContainsWords" kind:match |
OfficeWorkload | eq |
| field:"m365::Workload" kind:eq value:"Exchange" |
Operation | eq |
| field:"Operation" kind:eq value:"New-InboxRule" |
Parameters | match |
| field:"m365::Parameters" kind:match |
ResultStatus | eq |
| field:"ResultStatus" kind:eq |
SubjectContainsWords | match |
| field:"SubjectContainsWords" kind:match |
SubjectOrBodyContainsWords | match |
| field:"SubjectOrBodyContainsWords" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIPAddress | summarize |
EndTimeUtc | summarize |
Keyword | summarize |
OfficeObjectId | summarize |
Operation | summarize |
OriginatingServer | summarize |
ResultStatus | summarize |
RuleDetail | summarize |
StartTimeUtc | summarize |
UserId | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
OriginatingServerName | extend |
Netskope - High Severity Alert
#Detects Netskope alerts raised with a high or critical severity. High severity alerts typically indicate DLP violations, malware detections, compromised credentials, or significant policy breaches that warrant immediate investigation.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Exfiltration |
Rule Body
id: a1f6c2d4-8b35-4e19-9c7a-2d4e6f8a1b30
name: Netskope - High Severity Alert
description: |
Detects Netskope alerts raised with a high or critical severity. High severity alerts
typically indicate DLP violations, malware detections, compromised credentials, or
significant policy breaches that warrant immediate investigation.
severity: High
status: Available
requiredDataConnectors:
- connectorId: NetskopeAlertEventsConnector
dataTypes:
- NetskopeAlertEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Exfiltration
relevantTechniques:
- T1078
- T1567
query: |
NetskopeAlertEvents_CL
| where TimeGenerated > ago(1h)
| where Alert =~ "yes"
| where Severity in~ ("high", "critical")
| extend NormalizedSeverity = tolower(Severity)
| summarize
AlertCount = count(),
Activities = make_set(Activity, 20),
Applications = make_set(App, 20),
Policies = make_set(Policy, 20),
Actions = make_set(Action, 10),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by AlertName, AlertType, NormalizedSeverity, User, Userip, Hostname, SrcCountry
| order by AlertCount desc
| project
LastSeen,
AlertName,
AlertType,
Severity = NormalizedSeverity,
User,
Userip,
Hostname,
SrcCountry,
AlertCount,
Applications,
Activities,
Policies,
Actions,
FirstSeen
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: User
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Userip
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
NetskopeAlertEvents_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where Alert =~ "yes"
Stage 4: where
| where Severity in~ ("high", "critical")
Stage 5: extend
| extend NormalizedSeverity = tolower(Severity)
Stage 6: summarize
| summarize
AlertCount = count(),
Activities = make_set(Activity, 20),
Applications = make_set(App, 20),
Policies = make_set(Policy, 20),
Actions = make_set(Action, 10),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by AlertName, AlertType, NormalizedSeverity, User, Userip, Hostname, SrcCountry
Stage 7: sort
| order by AlertCount desc
Stage 8: project
| project
LastSeen,
AlertName,
AlertType,
Severity = NormalizedSeverity,
User,
Userip,
Hostname,
SrcCountry,
AlertCount,
Applications,
Activities,
Policies,
Actions,
FirstSeen
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Alert | eq |
| field:"Alert" kind:eq value:"yes" |
Severity | in |
| field:"Severity" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | project |
Activities | project |
AlertCount | project |
AlertName | project |
AlertType | project |
Applications | project |
FirstSeen | project |
Hostname | project |
LastSeen | project |
Policies | project |
Severity | project |
SrcCountry | project |
User | project |
Userip | project |
PaloAlto - Possible port scan
#'Detects possible port scan.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 3575a9c0-51c9-11ec-bf63-0242ac130002
name: PaloAlto - Possible port scan
description: |
'Detects possible port scan.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1595
query: |
let threshold = 10;
PaloAltoCDLEvent
| where EventResourceId =~ 'TRAFFIC'
| where isnotempty(DstPortNumber) and isnotempty(SrcIpAddr)
| summarize PortSet = make_set(DstPortNumber) by SrcIpAddr, bin(TimeGenerated, 5m)
| where array_length(PortSet) > threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
PaloAltoCDLEvent
Stage 2: where
| where EventResourceId =~ 'TRAFFIC'
Stage 3: where
| where isnotempty(DstPortNumber) and isnotempty(SrcIpAddr)
Stage 4: summarize
| summarize PortSet = make_set(DstPortNumber) by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 5: where
| where array_length(PortSet) > threshold
Stage 6: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstPortNumber | is_not_null | field:"DstPortNumber" kind:is_not_null | |
EventResourceId | eq |
| field:"EventResourceId" kind:eq value:"TRAFFIC" |
SrcIpAddr | is_not_null | field:"src_ip" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PortSet | summarize |
SrcIpAddr | summarize |
IPCustomEntity | extend |
Pathlock TDnR - J2EE Security Events
#Detects security events from SAP J2EE (Java) engine security logs, forwarded by Pathlock Threat Detection and Response. J2EE security anomalies may indicate web application attacks, unauthorized access to SAP NetWeaver Java services, or exploitation of Java-based SAP components.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Discovery |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c35
name: Pathlock TDnR - J2EE Security Events
kind: Scheduled
description: >-
Detects security events from SAP J2EE (Java) engine security logs, forwarded by Pathlock Threat
Detection and Response. J2EE security anomalies may indicate web application attacks, unauthorized
access to SAP NetWeaver Java services, or exploitation of Java-based SAP components.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Discovery
relevantTechniques:
- T1190
- T1082
query: |
Pathlock_TDnR_CL
| where DataSource == "J2EE_SECURITY_LOG"
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Bname
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AnyAlert
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Stage 1: source
Pathlock_TDnR_CL
Stage 2: where
| where DataSource == "J2EE_SECURITY_LOG"
Stage 3: project
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"J2EE_SECURITY_LOG" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Pathlock TDnR - SU24 Table USOBX_C Changes
#Detects changes to the SU24 authorization check table USOBX_C in SAP, forwarded by Pathlock Threat Detection and Response. USOBX_C controls whether authorization checks are active for transactions; unauthorized changes may disable security checks, enabling unauthorized access.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Defense Impairment |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c19
name: Pathlock TDnR - SU24 Table USOBX_C Changes
kind: Scheduled
description: >-
Detects changes to the SU24 authorization check table USOBX_C in SAP, forwarded by Pathlock Threat
Detection and Response. USOBX_C controls whether authorization checks are active for transactions;
unauthorized changes may disable security checks, enabling unauthorized access.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
relevantTechniques:
- T1562
- T1548
query: |
Pathlock_TDnR_CL
| where DataSource == "CHANGEDOC_USOBX_C"
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Bname
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AnyAlert
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Stage 1: source
Pathlock_TDnR_CL
Stage 2: where
| where DataSource == "CHANGEDOC_USOBX_C"
Stage 3: project
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"CHANGEDOC_USOBX_C" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Powershell Empire Cmdlets Executed in Command Line
#This query identifies use of PowerShell Empire's cmdlets within the command line data of the PowerShell process, indicating potential use of the post-exploitation tool.
MITRE ATT&CK Coverage
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4688: A new process has been created. |
Rule Body
id: ef88eb96-861c-43a0-ab16-f3835a97c928
name: Powershell Empire Cmdlets Executed in Command Line
description: |
'This query identifies use of PowerShell Empire's cmdlets within the command line data of the PowerShell process, indicating potential use of the post-exploitation tool.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- CommandAndControl
- CredentialAccess
- DefenseEvasion
- Discovery
- Execution
- Exfiltration
- LateralMovement
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1548.002
- T1134
- T1134.002
- T1134.005
- T1087.001
- T1087.002
- T1557.001
- T1071.001
- T1560
- T1547.001
- T1547.005
- T1547.009
- T1217
- T1115
- T1059
- T1059.001
- T1059.003
- T1136.001
- T1136.002
- T1543.003
- T1555.003
- T1484.001
- T1482
- T1114.001
- T1573.002
- T1546.008
- T1041
- T1567.001
- T1567.002
- T1068
- T1210
- T1083
- T1615
- T1574.001
- T1574.004
- T1574.007
- T1574.008
- T1574.009
- T1070.006
- T1105
- T1056.001
- T1056.004
- T1106
- T1046
- T1135
- T1040
- T1027
- T1003.001
- T1057
- T1055
- T1021.003
- T1021.004
- T1053.005
- T1113
- T1518.001
- T1558.002
- T1558.003
- T1082
- T1016
- T1049
- T1569.002
- T1127.001
- T1552.001
- T1552.004
- T1550.002
- T1125
- T1102.002
- T1047
query: |
let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));
(union isfuzzy=true
(SecurityEvent
| where EventID == 4688
//consider filtering on filename if perf issues occur
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
),
(WindowsEvent
| where EventID == 4688
| where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
//consider filtering on filename if perf issues occur
//extend NewProcessName = tostring(EventData.NewProcessName)
//extend Process=tostring(split(NewProcessName, '\\')[-1])
//FileName = Process
//where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
// Note: currently the base64_decode_tostring function is limited to supporting UTF8
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: SubjectUserName
- identifier: NTDomain
columnName: SubjectDomainName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.3.1
kind: Scheduled
Stages and Predicates
Let Binding: regexEmpire
let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent
Leg 1: SecurityEvent
SecurityEvent
| where EventID == 4688
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
Leg 2: WindowsEvent
WindowsEvent
| where EventID == 4688
| where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) - (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match value:"-encodedCommand" |
EfectiveCommand | regex_match |
| field:"EfectiveCommand" kind:regex_match value:"regexEmpire" |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ParentProcessName | match | gc_worker.exe, gc_service.exe | excludes:ParentProcessName field:"ParentProcessName" value:"gc_worker.exe" field:"ParentProcessName" value:"gc_service.exe" |
EventData | match | gc_worker.exe, gc_service.exe | excludes:EventData field:"EventData" value:"gc_worker.exe" field:"EventData" value:"gc_service.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CommandLine | project |
Computer | project |
EfectiveCommand | project |
FileName | project |
ParentProcessName | project |
SubjectDomainName | project |
SubjectUserName | project |
decodedCommand | project |
encodedCommand | project |
timestamp | project |
DnsDomain | extend |
HostName | extend |
SAP ETD - Synch 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.
Synch alerts coming in from SAP Enterprise Threat Detection into Microsoft Sentinel (one way)
Rule Body
id: 7a830484-e349-4527-85f6-7850c468c238
kind: Scheduled
name: SAP ETD - Synch alerts
description: Synch alerts coming in from SAP Enterprise Threat Detection into Microsoft Sentinel (one way)
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPETDAlerts
dataTypes:
- SAPETDAlerts_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
let minThreshold= 1;
let minScore= 50;
let lookBack= 7d;
let regex_sid = @"^([A-Z0-9]{3})/";
let regex_client = @'\/(.{3})$';
SAPETDAlerts_CL
| mv-expand NormalizedTriggeringEvents
| summarize arg_max(TimeGenerated, *) by AlertId
| where Threshold >= minThreshold and Score >= minScore
| extend
SystemId= extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
ClienId= extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
Host= NormalizedTriggeringEvents.NetworkHostnameInitiator,
Instance= NormalizedTriggeringEvents.NetworkHostnameActor,
User= NormalizedTriggeringEvents.UserAccountActing,
IP= NormalizedTriggeringEvents.NetworkIPAddressInitiator
| mv-expand Users
| extend
UserAccountName = tostring(Users.UserAccountName),
UserEmail = tostring(Users.EmailAddresses[0]);
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserAccountName
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: UserEmail
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: SystemId
- identifier: AppId
columnName: ClienId
- identifier: InstanceName
columnName: Instance
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Host
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP
alertDetailsOverride:
alertDisplayNameFormat: 'SAP ETD - {{PatternName}} '
alertDescriptionFormat: '{{PatternDescription}}'
customDetails:
SAP_User: User
SAP_UserEmail: UserEmail
ETD_AlertNumber: AlertId
version: 1.0.4
Stages and Predicates
Parameters
let minThreshold = 1;
let minScore = 50;
let lookBack = 7d;
let regex_sid = @"^([A-Z0-9]{3})/";
let regex_client = @'\/(.{3})$';
Stage 1: source
SAPETDAlerts_CL
Stage 2: mv-expand
| mv-expand NormalizedTriggeringEvents
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by AlertId
Stage 4: where
| where Threshold >= minThreshold and Score >= minScore
Stage 5: extend
| extend
SystemId= extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
ClienId= extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
Host= NormalizedTriggeringEvents.NetworkHostnameInitiator,
Instance= NormalizedTriggeringEvents.NetworkHostnameActor,
User= NormalizedTriggeringEvents.UserAccountActing,
IP= NormalizedTriggeringEvents.NetworkIPAddressInitiator
Stage 6: mv-expand
| mv-expand Users
Stage 7: extend
| extend
UserAccountName = tostring(Users.UserAccountName),
UserEmail = tostring(Users.EmailAddresses[0]);
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Score | ge |
| field:"Score" kind:ge value:"50" |
Threshold | ge |
| field:"Threshold" kind:ge value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertId | summarize |
ClienId | extend |
Host | extend |
IP | extend |
Instance | extend |
SystemId | extend |
User | extend |
UserAccountName | extend |
UserEmail | extend |
Semperis DSP Kerberos krbtgt account with old password
#'The krbtgt user account is a special (disabled) user account in every Active Directory domain that has a special role in Kerberos function. If this account's password is compromised, Golden Ticket attacks can be performed to get access to any resource in the AD domain. This indicator looks for a krbtgt user account whose password hasn't been changed in the past 180 days. While Microsoft recommends changing the password every year, STIG recommends changing it every 180 days.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 9ff3b26b-7636-412e-ac46-072b084b94cb
name: Semperis DSP Kerberos krbtgt account with old password
description: |
'The krbtgt user account is a special (disabled) user account in every Active Directory domain that has a special role in Kerberos function. If this account's password is compromised, Golden Ticket attacks can be performed to get access to any resource in the AD domain. This indicator looks for a krbtgt user account whose password hasn't been changed in the past 180 days. While Microsoft recommends changing the password every year, STIG recommends changing it every 180 days.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SemperisDSP
dataTypes:
- dsp_parser
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1558.001
query: |
dsp_parser
| where EventID == 9212
| where SecurityIndicatorName == "Kerberos krbtgt account with old password"
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 2.0.7
kind: Scheduled
Stages and Predicates
Stage 1: source
dsp_parser
Stage 2: where
| where EventID == 9212
Stage 3: where
| where SecurityIndicatorName == "Kerberos krbtgt account with old password"
Stage 4: extend
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"9212" |
SecurityIndicatorName | eq |
| field:"SecurityIndicatorName" kind:eq value:"Kerberos krbtgt account with old password" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
TI map Email entity to OfficeActivity
#'Identifies a match in OfficeActivity table from any Email IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 4a3f5ed7-8da5-4ce2-af6f-c9ada45060f2
name: TI map Email entity to OfficeActivity
description: |
'Identifies a match in OfficeActivity table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity
- 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 emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
let OfficeEvents = materialize(
OfficeActivity
| where isnotempty(UserId)
| where TimeGenerated >= ago(dt_lookBack)
| where UserId matches regex emailregex
| project-rename OfficeActivity_TimeGenerated = TimeGenerated);
let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);
ThreatIntelligenceIndicator
| where isnotempty(EmailSenderAddress)
| where TimeGenerated >= ago(ioc_lookBack)
| where tolower(EmailSenderAddress) in (OfficeActivityUPNs)
| 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;"
// 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 (OfficeEvents) on $left.EmailSenderAddress == $right.UserId
| where OfficeActivity_TimeGenerated < ExpirationDateTime
| summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, UserId
| project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, UserId, ClientIP, Operation, UserType, RecordType, OfficeWorkload, Parameters
| extend Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])
| extend timestamp = OfficeActivity_TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.2.7
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
Let Binding: OfficeEvents
let OfficeEvents = materialize(
OfficeActivity
| where isnotempty(UserId)
| where TimeGenerated >= ago(dt_lookBack)
| where UserId matches regex emailregex
| project-rename OfficeActivity_TimeGenerated = TimeGenerated);
Let Binding: OfficeActivityUPNs
let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where isnotempty(EmailSenderAddress)
Stage 3: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 4: where
| where tolower(EmailSenderAddress) in (OfficeActivityUPNs)
Stage 5: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 6: where
| where Active == true and ExpirationDateTime > now()
Stage 7: where
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
Stage 8: join
| join kind=innerunique (OfficeEvents) on $left.EmailSenderAddress == $right.UserId
Stage 9: where
where OfficeActivity_TimeGenerated < ExpirationDateTime
Stage 10: summarize
summarize OfficeActivity_TimeGenerated by IndicatorId, UserId
Stage 11: project
project ActivityGroupNames, ClientIP, ConfidenceScore, Description, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, OfficeActivity_TimeGenerated, OfficeWorkload, Operation, Parameters, RecordType, ThreatType, Url, UserId, UserType
Stage 12: extend
extend Name, UPNSuffix
Stage 13: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
EmailSenderAddress | in |
| field:"EmailSenderAddress" kind:in value:"OfficeActivityUPNs" |
EmailSenderAddress | is_not_null | field:"EmailSenderAddress" kind:is_not_null | |
UserId | is_not_null | field:"UserId" kind:is_not_null | |
UserId | regex_match |
| field:"UserId" kind:regex_match value:"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$" |
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;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ClientIP | project |
ConfidenceScore | project |
Description | project |
EmailRecipient | project |
EmailSenderName | project |
EmailSourceDomain | project |
EmailSourceIpAddress | project |
EmailSubject | project |
ExpirationDateTime | project |
FileHashType | project |
FileHashValue | project |
IndicatorId | project |
OfficeActivity_TimeGenerated | project |
OfficeWorkload | project |
Operation | project |
Parameters | project |
RecordType | project |
ThreatType | project |
Url | project |
UserId | project |
UserType | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
TI Map File Hash to Security Event
#Identifies a match in Security Event data from any File Hash IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
Rule Body
id: 9f7dc779-1e51-4925-ae4a-db1db933077f
name: TI Map File Hash to Security Event
description: |
'Identifies a match in Security Event data from any File Hash IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
ThreatIntelIndicators
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "file"
| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
| extend FileHashValue = toupper(ObservableValue)
| where TimeGenerated >= ago(ioc_lookBack)
| extend FileHashValue = toupper(FileHashValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| project-reorder *, FileHashType, FileHashValue, Type
// 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 ( union isfuzzy=true
(SecurityEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(FileHash)
),
(WindowsEvent | where TimeGenerated >= ago(dt_lookBack)
| where EventID in ("8003","8002","8005")
| extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(EventData.FileHash)
)
)
on $left.FileHashValue == $right.FileHash
| where SecurityEvent_TimeGenerated < ValidUntil
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, FileHash
| project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ValidUntil, Confidence,
Process, FileHash, Computer, Account, Event, FileHashValue, FileHashType, Url
| extend NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0])
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
| extend timestamp = SecurityEvent_TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: FileHash
fieldMappings:
- identifier: Value
columnName: FileHashValue
- identifier: Algorithm
columnName: FileHashType
version: 1.4.10
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType == "file"
Stage 4: extend
| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
Stage 5: extend
| extend FileHashValue = toupper(ObservableValue)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: extend (3 consecutive steps)
| extend FileHashValue = toupper(FileHashValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
Stage 8: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 9: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 10: project-reorder
| project-reorder *, FileHashType, FileHashValue, Type
Stage 11: join
join kind=innerunique (...) on $left.FileHashValue, $right.FileHash
Stage 12: where
where SecurityEvent_TimeGenerated < ValidUntil
Stage 13: extend
extend Description
Stage 14: extend
extend ActivityGroupNames
Stage 15: summarize
summarize SecurityEvent_TimeGenerated by IndicatorId, FileHash
Stage 16: project
project Account, ActivityGroupNames, Computer, Confidence, Description, Event, FileHash, FileHashType, FileHashValue, IndicatorId, Process, SecurityEvent_TimeGenerated, Url, ValidUntil
Stage 17: extend (3 consecutive steps)
extend DnsDomain, HostName, NTDomain, Name, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | in |
| field:"EventID" kind:in |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"file" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityGroupNames | project |
Computer | project |
Confidence | project |
Description | project |
Event | project |
FileHash | project |
FileHashType | project |
FileHashValue | project |
IndicatorId | project |
Process | project |
SecurityEvent_TimeGenerated | project |
Url | project |
ValidUntil | project |
NTDomain | extend |
Name | extend |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
TIE user accounts issues
#'Searches for triggered Indicators of Exposures related to user accounts issues.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: c4562ef3-d821-4089-b6c0-120d95c855e6
name: TIE user accounts issues
description: |
'Searches for triggered Indicators of Exposures related to user accounts issues.'
severity: Low
requiredDataConnectors:
- connectorId: TenableIE
dataTypes:
- Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
// For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
// Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
// Then, create the Kusto Function with alias afad_parser
let SeverityTable=datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];
afad_parser
| where MessageType == 0 and Codename in~ (codeNameList)
| lookup kind=leftouter SeverityTable on Severity
| order by Level
| 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
Let Binding: SeverityTable
let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];
Let Binding: codeNameList
let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 0 and Codename in~ (codeNameList)
Stage 3: kusto:lookup
lookup kind=leftouter (SeverityTable) on Severity
Stage 4: sort
sort by Level
Stage 5: extend
extend DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | in |
| field:"Codename" kind:in value:"codeNameList" |
MessageType | eq |
| field:"MessageType" kind:eq value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
User Assigned New Privileged Role
#Identifies when a new eligible or active privileged role is assigned to a user. Does not alert on PIM activations. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
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: 746ddb63-f51b-4563-b449-a8b13cf302ec
name: User Assigned New Privileged Role
description: |
'Identifies when a new eligible or active privileged role is assigned to a user. Does not alert on PIM activations. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Persistence
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
AuditLogs
| where Category =~ "RoleManagement"
| where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-apply TargetResourceSubject = TargetResources on
(
where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
| extend Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
subjectProps = TargetResourceSubject.modifiedProperties
)
| mv-apply TargetResourceRole = TargetResources on
(
// mimic modifiedProperties so we can use the same logic to get the role name regardless of where it comes from
where TargetResourceRole.type in~ ("Role")
| extend roleProps = pack_array(bag_pack("displayName","Role.DisplayName", "newValue", TargetResourceRole.displayName))
)
| mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) 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(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
// Comment below to alert for PIM activations
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| summarize by bin(TimeGenerated, 1h), OperationName, RoleName, Target, Initiator, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result
| extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0]), InitiatorName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Target
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatorName
- identifier: UPNSuffix
columnName: InitiatorUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")
Stage 4: where
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
Stage 5: kusto:mv-apply
| mv-apply TargetResourceSubject = TargetResources on
(
where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
| extend Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
subjectProps = TargetResourceSubject.modifiedProperties
)
Stage 6: kusto:mv-apply
| mv-apply TargetResourceRole = TargetResources on
(
where TargetResourceRole.type in~ ("Role")
| extend roleProps = pack_array(bag_pack("displayName","Role.DisplayName", "newValue", TargetResourceRole.displayName))
)
Stage 7: kusto:mv-apply
| mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"',tostring(Property.newValue))
)
Stage 8: where
| where RoleName contains "Admin"
Stage 9: extend (6 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(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
Stage 10: where
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
Stage 11: summarize
| summarize by bin(TimeGenerated, 1h), OperationName, RoleName, Target, Initiator, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result
Stage 12: extend
| extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0]), InitiatorName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | in |
| field:"AADOperationType" kind:in |
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Initiator | ne |
| field:"Initiator" kind:ne |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"Role.DisplayName" |
RoleName | contains |
| field:"RoleName" kind:contains value:"Admin" |
TargetResourceRole.type | in |
| field:"TargetResourceRole.type" kind:in value:"Role" |
TargetResourceSubject.type | in |
| field:"TargetResourceSubject.type" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAadUserId | summarize |
InitiatingAppName | summarize |
InitiatingAppServicePrincipalId | summarize |
InitiatingIpAddress | summarize |
InitiatingUserPrincipalName | summarize |
Initiator | summarize |
OperationName | summarize |
Result | summarize |
RoleName | summarize |
Target | summarize |
InitiatorName | extend |
InitiatorUPNSuffix | extend |
TargetName | extend |
TargetUPNSuffix | extend |