Detection rules › Kusto
Kusto rules: 2r
A host is potentially running a crypto miner (ASIM Web Session schema)
#'This rule identifies a web request with a user agent header known to belong to a crypto miner. This indicates a crypto miner may have infected the client machine.<br>You can add custom crypto mining indicating User-Agent headers using a watchlist, for more information refer to the UnusualUserAgents Watchlist.<br><br> This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 8cbc3215-fa58-4bd6-aaaa-f0029c351730
name: A host is potentially running a crypto miner (ASIM Web Session schema)
description: |
'This rule identifies a web request with a user agent header known to belong to a crypto miner. This indicates a crypto miner may have infected the client machine.<br>You can add custom crypto mining indicating User-Agent headers using a watchlist, for more information refer to the [UnusualUserAgents Watchlist](https://aka.ms/ASimUnusualUserAgentsWatchlist).<br><br> This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
severity: Medium
tags:
- ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/CiscoUmbrella/CiscoUmbrellaCryptoMinerUserAgentDetected.yaml
version: 1.0.0
- Schema: ASimWebSession
SchemaVersion: 0.2.1
requiredDataConnectors:
- connectorId: SquidProxy
dataTypes:
- SquidProxy_CL
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1496
query: |
let threatCategory="Cryptominer";
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
let knownUserAgents=toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let customUserAgents=toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let fullUAList = array_concat(knownUserAgents,customUserAgents);
_Im_WebSession(httpuseragent_has_any=fullUAList)
| summarize N_Events=count() by SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1])
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SrcUsername
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
alertDetailsOverride:
alertDisplayNameFormat: The host {{SrcIpAddr}} is potentially running a crypto miner
alertDescriptionFormat: The host at address {{SrcIpAddr}} sent an HTTP request to the URL {{Url}} with the HTTP user agent header {{HttpUserAgent}}. This user agent is known to be used by crypto miners and indicates crypto mining activity on the client.
customDetails:
UserAgent: HttpUserAgent
eventGroupingSettings:
aggregationKind: AlertPerResult
version: 1.1.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yaron
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Parameters
let threatCategory = "Cryptominer";
let fullUAList = array_concat(knownUserAgents,customUserAgents);
Let Binding: knownUserAgentsIndicators
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
Let Binding: knownUserAgents
let knownUserAgents = toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
Let Binding: customUserAgents
let customUserAgents = toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
Stage 1: source
_Im_WebSession(httpuseragent_has_any=fullUAList)
Stage 2: summarize
| summarize N_Events=count() by SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
Stage 3: extend
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1])
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HttpUserAgent | summarize |
N_Events | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
TimeGenerated | summarize |
Url | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
AIShield - Tabular classification AI Model Evasion low suspicious vulnerability detection
#'This alert creates an incident when Tabular classification AI Model Evasion Low suspicious, medium severity vulnerability detected from the AIShield.'
Rule Body
id: af245eff-0db9-4df8-82e6-998185cac332
name: AIShield - Tabular classification AI Model Evasion low suspicious vulnerability detection
description: |
'This alert creates an incident when Tabular classification AI Model Evasion Low suspicious, medium severity vulnerability detected from the AIShield.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoschAIShield
dataTypes:
- AIShield
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
AIShield
| where Message has 'Tabular Classification AI Model Evasion Attack Identified'
| where Severity =~ 'Medium'
| where SuspiciousLevel =~ 'low suspicious attack'
| extend NTDomain = tostring(split(Computer, '\\')[0]), HostName = tostring(split(Computer, '.')[0])
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: NTDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceName
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: AIShield - Tabular Classification AI Model Evasion low suspicious vulnerability detected.
alertDescriptionFormat: |
This query detects Tabular Classification AI Model Evasion low suspicious, medium severity alert from AIShield generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
alertTacticsColumnName: null
alertSeverityColumnName: Severity
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AIShield
Stage 2: where
| where Message has 'Tabular Classification AI Model Evasion Attack Identified'
Stage 3: where
| where Severity =~ 'Medium'
Stage 4: where
| where SuspiciousLevel =~ 'low suspicious attack'
Stage 5: extend
| extend NTDomain = tostring(split(Computer, '\\')[0]), HostName = tostring(split(Computer, '.')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Message | match |
| field:"Message" kind:match value:"Tabular Classification AI Model Evasion Attack Identified" |
Severity | eq |
| field:"Severity" kind:eq value:"Medium" |
SuspiciousLevel | eq |
| field:"SuspiciousLevel" kind:eq value:"low suspicious attack" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
NTDomain | extend |
AIShield - Timeseries Forecasting AI Model extraction high suspicious vulnerability detection
#'This alert creates an incident when Timeseries Forecasting AI Model extraction high suspicious, high severity vulnerability detected from the AIShield.'
Rule Body
id: 37118ef6-73b4-49aa-b13b-cdeeeea580df
name: AIShield - Timeseries Forecasting AI Model extraction high suspicious vulnerability detection
description: |
'This alert creates an incident when Timeseries Forecasting AI Model extraction high suspicious, high severity vulnerability detected from the AIShield.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: BoschAIShield
dataTypes:
- AIShield
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
AIShield
| where Message has 'Timeseries Forecasting AI Model Extraction Attack Identified'
| where Severity =~ 'High'
| where SuspiciousLevel =~ 'high suspicious attack'
| extend NTDomain = tostring(split(Computer, '\\')[0]), HostName = tostring(split(Computer, '.')[0])
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: NTDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceName
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: AIShield -Timeseries Forecasting AI Model extraction high suspicious vulnerability detected.
alertDescriptionFormat: |
This query detects Timeseries Forecasting AI Model extraction high suspicious, high severity alert from AIShield generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
alertTacticsColumnName: null
alertSeverityColumnName: Severity
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AIShield
Stage 2: where
| where Message has 'Timeseries Forecasting AI Model Extraction Attack Identified'
Stage 3: where
| where Severity =~ 'High'
Stage 4: where
| where SuspiciousLevel =~ 'high suspicious attack'
Stage 5: extend
| extend NTDomain = tostring(split(Computer, '\\')[0]), HostName = tostring(split(Computer, '.')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Message | match |
| field:"Message" kind:match value:"Timeseries Forecasting AI Model Extraction Attack Identified" |
Severity | eq |
| field:"Severity" kind:eq value:"High" |
SuspiciousLevel | eq |
| field:"SuspiciousLevel" kind:eq value:"high suspicious attack" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
NTDomain | extend |
Alsid Password Spraying
#'Searches for Password spraying attacks'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 9e20eb4e-cc0d-4349-a99d-cad756859dfb
name: Alsid Password Spraying
description: |
'Searches for Password spraying attacks'
severity: High
requiredDataConnectors:
- connectorId: AlsidForAD
dataTypes:
- AlsidForADLog_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110.003
query: |
afad_parser
| where MessageType == 2 and Codename == "Password Spraying"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "Password Spraying"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"Password Spraying" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
AWSCloudTrail - Privilege escalation via DataPipeline policy
#Detects inline IAM policy updates that combine Data Pipeline permissions with IAM privileges such as iam:PassRole. This sequence can enable indirect execution and elevated access pathways, indicating potential privilege escalation activity.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
Rule Body
id: 48896551-1c28-4a09-8388-e51e5a927d23
name: AWSCloudTrail - Privilege escalation via DataPipeline policy
description: |
Detects inline IAM policy updates that combine Data Pipeline permissions with IAM privileges such as
iam:PassRole. This sequence can enable indirect execution and elevated access pathways, indicating potential
privilege escalation activity.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1098.003
query: |
AWSCloudTrail
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
| mvexpand Statement
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
| extend Action = tostring(Action)
| where Effect =~ "Allow" and (((Action has "iam:*" or Action has "iam:PassRole") and Action has "datapipeline:*") or ((Action has "iam:*" or Action has "iam:PassRole") and Action has "datapipeline:CreatePipeline" and Action has "datapipeline:PutPipelineDefinition" and Action has "datapipeline:ActivatePipeline") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:Create*" and Action contains "datapipeline:Put*" and Action contains "datapipeline:Activate*")) and Resource == "*" and isempty(Condition)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
PolicyName: PolicyName
EventName: EventName
UserIdentityArn: UserIdentityArn
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS Data Pipeline privilege escalation policy update by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline Data Pipeline escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 4: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 5: mv-expand
| mvexpand Statement
Stage 6: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
Stage 7: extend
| extend Action = tostring(Action)
Stage 8: where
| where Effect =~ "Allow" and (((Action has "iam:*" or Action has "iam:PassRole") and Action has "datapipeline:*") or ((Action has "iam:*" or Action has "iam:PassRole") and Action has "datapipeline:CreatePipeline" and Action has "datapipeline:PutPipelineDefinition" and Action has "datapipeline:ActivatePipeline") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:Create*" and Action contains "datapipeline:Put*" and Action contains "datapipeline:Activate*")) and Resource == "*" and isempty(Condition)
Stage 9: extend (4 consecutive steps)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
Stage 10: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Action | match |
| field:"Action" kind:match |
Condition | is_null | field:"Condition" kind:is_null | |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
BloodHound Attack Path Finding - App Admin Control of Tier Zero Principal
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - App Admin Control of Tier Zero Principal
Rule Body
id: ba785c29-37f7-469b-b437-495bfdf136eb
name: BloodHound Attack Path Finding - App Admin Control of Tier Zero Principal
description: BloodHound Attack Path Finding - App Admin Control of Tier Zero Principal
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "AzureT0AppAdmin"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "AzureT0AppAdmin"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0AppAdmin" |
BloodHound Attack Path Finding - Owner Role on Tier Zero Resource
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Owner Role on Tier Zero Resource
Rule Body
id: cc168d7f-59d1-4472-be98-854869e62907
name: BloodHound Attack Path Finding - Owner Role on Tier Zero Resource
description: BloodHound Attack Path Finding - Owner Role on Tier Zero Resource
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "AzureT0Owner"
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "AzureT0Owner"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0Owner" |
Cisco Cloud Security - Rare User Agent Detected
#'Rule helps to detect a rare user-agents indicating web browsing activity by an unusual process other than a web browser.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
Rule Body
id: 8c8de3fa-6425-4623-9cd9-45de1dd0569a
name: Cisco Cloud Security - Rare User Agent Detected
description: |
'Rule helps to detect a rare user-agents indicating web browsing activity by an unusual process other than a web browser.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_proxy_CL
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- Exfiltration
relevantTechniques:
- T1071.001
- T1041
query: |
let user_agents_list = Cisco_Umbrella
| where EventType == "proxylogs"
| summarize count() by HttpUserAgentOriginal
| summarize make_list(HttpUserAgentOriginal);
Cisco_Umbrella
| where EventType == "proxylogs"
| where HttpUserAgentOriginal !in (user_agents_list)
| extend Message = "Rare User Agent"
| project TimeGenerated, Message, SrcIpAddr, DstIpAddr, UrlOriginal, HttpUserAgentOriginal
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlOriginal
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.1.3
kind: Scheduled
Stages and Predicates
Let Binding: user_agents_list
let user_agents_list = Cisco_Umbrella
| where EventType == "proxylogs"
| summarize count() by HttpUserAgentOriginal
| summarize make_list(HttpUserAgentOriginal);
Stage 1: source
Cisco_Umbrella
Stage 2: where
| where EventType == "proxylogs"
Stage 3: where
| where HttpUserAgentOriginal !in (user_agents_list)
Stage 4: extend
| extend Message = "Rare User Agent"
Stage 5: project
| project TimeGenerated, Message, SrcIpAddr, DstIpAddr, UrlOriginal, HttpUserAgentOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"proxylogs" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
HttpUserAgentOriginal | eq | user_agents_list | excludes:HttpUserAgentOriginal field:"HttpUserAgentOriginal" value:"user_agents_list" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstIpAddr | project |
HttpUserAgentOriginal | project |
Message | project |
SrcIpAddr | project |
TimeGenerated | project |
UrlOriginal | project |
CYFIRMA - Attack Surface - Cloud Weakness 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 rule detects cloud storage buckets (e.g., AWS S3) that are publicly accessible without authentication. Such misconfigurations can lead to data exfiltration, compliance violations, and reputational damage. The detection is based on Cyfirma's Attack Surface Intelligence."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 87cd8b10-90f6-4967-a4a7-2142e848ec8f
name: CYFIRMA - Attack Surface - Cloud Weakness High Rule
description: |
"This rule detects cloud storage buckets (e.g., AWS S3) that are publicly accessible without authentication.
Such misconfigurations can lead to data exfiltration, compliance violations, and reputational damage.
The detection is based on Cyfirma's Attack Surface Intelligence."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaAttackSurfaceAlertsConnector
dataTypes:
- CyfirmaASCloudWeaknessAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Collection
- Discovery
- Exfiltration
relevantTechniques:
- T1087
- T1087.004
query: |
// High Severity - Attack Surface - Cloud Weakness - Unauthorized Public Cloud Storage Exposure Detected
let timeFrame = 5m;
CyfirmaASCloudWeaknessAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=asset_name,
AlertUID=alert_uid,
UID=uid,
Source=source,
SourceType=source_type,
CreatedDate=created_date,
Impact=impact,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
Domain,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
Source,
SourceType,
CreatedDate,
Impact,
ProviderName,
ProductName
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
customDetails:
TimeGenerated: TimeGenerated
RiskScore: RiskScore
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
Source: Source
SourceType: SourceType
CreatedDate: CreatedDate
Impact: Impact
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Alert - Unauthorized Public Cloud Storage Exposure Detected - Domain: {{Domain}} "
alertDescriptionFormat: "CYFIRMA - High Severity Alert - Unauthorized Public Cloud Storage Exposure Detected - {{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
CyfirmaASCloudWeaknessAlerts_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,
Domain=asset_name,
AlertUID=alert_uid,
UID=uid,
Source=source,
SourceType=source_type,
CreatedDate=created_date,
Impact=impact,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Description,
Domain,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
Source,
SourceType,
CreatedDate,
Impact,
ProviderName,
ProductName
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 |
CreatedDate | project |
Description | project |
Domain | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
Source | project |
SourceType | project |
TimeGenerated | project |
UID | project |
CYFIRMA - Medium severity Command & Control Network Indicators with Block Recommendation 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 analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control | |
| Exfiltration |
Rule Body
id: b278be0f-0662-47b4-9ab5-71a0f1435c3f
name: CYFIRMA - Medium severity Command & Control Network Indicators with Block Recommendation Rule
description: |
"This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."
version: 1.0.1
kind: Scheduled
severity: Medium
enabled: false
requiredDataConnectors:
- connectorId: CyfirmaCyberIntelligenceDC
dataTypes:
- CyfirmaIndicators_CL
query: |
// Network Indicators query with Block Recommended Action and C2 Roles
let timeFrame= 5m;
CyfirmaIndicators_CL
| where (ConfidenceScore < 80 and ConfidenceScore >= 50)
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains 'c2' or Roles contains 'Command & Control')
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
| mv-expand extensionKeys
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
- CommandAndControl
- InitialAccess
- Exfiltration
relevantTechniques:
- T1071
- T1090
- T1573
- T1105
- T1568
- T1566
- T1041
- T1071.001
- T1573.001
- T1568.002
- T1566.002
alertDetailsOverride:
alertDisplayNameFormat: "High-Confidence Command & Control Network Indicators with Block Recommendation Rule - {{name}} "
alertDescriptionFormat: "{{Description}} - {{name}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
customDetails:
ThreatActors: ThreatActors
Sources: Sources
RecommendedActions: RecommendedActions
Roles: Roles
Country: Country
Description: Description
ConfidenceScore: ConfidenceScore
SecurityVendors: SecurityVendors
IndicatorID: IndicatorID
Created: created
Modified: modified
ValidFrom: valid_from
Tags: Tags
ThreatType: ThreatType
TimeGenerated: TimeGenerated
IPAbuse: IPAbuse
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv4
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv6
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
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
CyfirmaIndicators_CL
Stage 2: where
| where (ConfidenceScore < 80 and ConfidenceScore >= 50)
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains 'c2' or Roles contains 'Command & Control')
Stage 3: extend (6 consecutive steps)
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
Stage 4: mv-expand
| mv-expand extensionKeys
Stage 5: extend (4 consecutive steps)
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
Stage 6: project
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfidenceScore | ge |
| field:"ConfidenceScore" kind:ge value:"50" |
ConfidenceScore | lt |
| field:"ConfidenceScore" kind:lt value:"80" |
RecommendedActions | match |
| field:"RecommendedActions" kind:match value:"Block" |
Roles | contains |
| field:"Roles" kind:contains |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
Cyren Feed Outage Detection
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'Detects when the Cyren threat intelligence feed has not ingested any data for 6 or more hours. This may indicate a connectivity issue with the data connector, API authentication problems, or upstream service disruption.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 7f9a0d5c-3b4c-6d7e-1f2a-e3f4a5b6c7d8
name: Cyren Feed Outage Detection
description: |
'Detects when the Cyren threat intelligence feed has not ingested any data for 6 or more hours.
This may indicate a connectivity issue with the data connector, API authentication problems, or upstream service disruption.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CyrenThreatIntel
dataTypes:
- Cyren_Indicators_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
Cyren_Indicators_CL
| where TimeGenerated >= ago(24h)
| summarize
Total = count(),
Latest = max(TimeGenerated)
| extend HoursAgo = datetime_diff('hour', now(), Latest)
| where isnotempty(Latest) and HoursAgo >= 6
| project
LatestEventTime = Latest,
HoursSinceLastEvent = HoursAgo,
TotalEventsLast24h = Total,
AlertMessage = strcat("Cyren feed has not received data for ", HoursAgo, " hours")
suppressionDuration: 1h
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: 1d
matchingMethod: Selected
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
LatestEventTime: LatestEventTime
HoursSinceLastEvent: HoursSinceLastEvent
TotalEventsLast24h: TotalEventsLast24h
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Cyren_Indicators_CL
Stage 2: where
| where TimeGenerated >= ago(24h)
Stage 3: summarize
| summarize
Total = count(),
Latest = max(TimeGenerated)
Stage 4: extend
| extend HoursAgo = datetime_diff('hour', now(), Latest)
Stage 5: where
| where isnotempty(Latest) and HoursAgo >= 6
Stage 6: project
| project
LatestEventTime = Latest,
HoursSinceLastEvent = HoursAgo,
TotalEventsLast24h = Total,
AlertMessage = strcat("Cyren feed has not received data for ", HoursAgo, " hours")
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HoursAgo | ge |
| field:"HoursAgo" kind:ge value:"6" |
Latest | is_not_null | field:"Latest" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertMessage | project |
HoursSinceLastEvent | project |
LatestEventTime | project |
TotalEventsLast24h | project |
Detect changes to Connect Sync Application
#This detection flags any changes happening on the Connect Sync Application in Entra ID. Since this is a very interesting account for attackers to abuse when moving laterally between AD DS and Entra ID, any change to the account should be investigated. We try to exclude legitimate certificate renewal processes, and a new account onboarding in the detection rule.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth | |
| Defense Impairment | |
| Credential Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rule Body
// Flag everything except a renewal process and onboarding
let base = materialize (
AuditLogs
// Search events happening on the Sync Account
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
// Only get cretificate or secret changes
| where OperationName contains "Update application – Certificates and secrets management"
// Expand the target resources and modified properties, and only use events ralted to KeyDescription
| mv-expand TargetResources
| extend ModifiedProperties = TargetResources.modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName == "KeyDescription"
// Save the old and new values of the secrets on the application
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
// Save the old and new credential names in an array
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
let newCredsAdd = (
base
// Flag when there are more new credentials than old ones
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
| project-rename NewCredAddTimeGenerated = TimeGenerated
);
let credRemove = (
base
// Get events where credentials are being removed
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
let credRenewal = (
// Find legitimate credential renewals
newCredsAdd
| join kind=leftouter credRemove on AppName
| where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);
AuditLogs
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
| join kind=leftanti credRenewal on CorrelationId
// Exclude cred removes (duplicate / not interesting)
| join kind=leftanti credRemove on CorrelationId
// Exclude new deployment
| where OperationName !in ("Add service principal", "Add application")
Stages and Predicates
Let Binding: base
let base = materialize (
AuditLogs
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
| where OperationName contains "Update application – Certificates and secrets management"
| mv-expand TargetResources
| extend ModifiedProperties = TargetResources.modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName == "KeyDescription"
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
Let Binding: newCredsAdd
let newCredsAdd = (
base
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
| project-rename NewCredAddTimeGenerated = TimeGenerated
);
Let Binding: credRemove
let credRemove = (
base
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
Let Binding: credRenewal
let credRenewal = (
newCredsAdd
| join kind=leftouter credRemove on AppName
| where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);
Stage 1: source
AuditLogs
Stage 2: extend
| extend AppName = tostring(TargetResources[0].displayName)
Stage 3: where
| where AppName startswith "ConnectSyncProvisioning_"
Stage 4: join (negated)
join kind=leftanti (credRenewal) on CorrelationId
Stage 5: join (negated)
join kind=leftanti (credRemove) on CorrelationId
Stage 6: where
where not (OperationName in ("Add application", "Add service principal"))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppName | starts_with |
| field:"AppName" kind:starts_with value:"ConnectSyncProvisioning_" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AppName | starts_with | ConnectSyncProvisioning_ | excludes:AppName field:"AppName" value:"ConnectSyncProvisioning_" |
ModifiedProperties.displayName | eq | KeyDescription | excludes:ModifiedProperties.displayName field:"ModifiedProperties.displayName" value:"KeyDescription" |
OldCredentialNames | cross_field_compare | NewCredentialNames | excludes:OldCredentialNames field:"OldCredentialNames" value:"NewCredentialNames" |
OperationName | contains | Update application – Certificates and secrets management | excludes:OperationName field:"OperationName" value:"Update application – Certificates and secrets management" |
OperationName | in | Add application, Add service principal | excludes:OperationName field:"OperationName" value:"Add application" field:"OperationName" value:"Add service principal" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AppName | extend |
Failed login attempts to Azure Portal
#Identifies failed login attempts in the Microsoft Entra ID SigninLogs to the Azure Portal. Many failed logon attempts or some failed logon attempts from multiple IPs could indicate a potential brute force attack. The following are excluded due to success and non-failure results: References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes 0 - successful logon 50125 - Sign-in was interrupted due to a password reset or password registration entry. 50140 - This error occurred due to 'Keep me signed in' interrupt when the user was signing-in.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
Rule Body
id: 223db5c1-1bf8-47d8-8806-bed401b356a4
name: Failed login attempts to Azure Portal
description: |
'Identifies failed login attempts in the Microsoft Entra ID SigninLogs to the Azure Portal. Many failed logon attempts or some failed logon attempts from multiple IPs could indicate a potential brute force attack.
The following are excluded due to success and non-failure results:
References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes
0 - successful logon
50125 - Sign-in was interrupted due to a password reset or password registration entry.
50140 - This error occurred due to 'Keep me signed in' interrupt when the user was signing-in.'
severity: Low
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let timeRange = 1d;
let lookBack = 7d;
let threshold_Failed = 5;
let threshold_FailedwithSingleIP = 20;
let threshold_IPAddressCount = 2;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let aadFunc = (tableName:string){
let azPortalSignins = materialize(table(tableName)
| where TimeGenerated >= ago(lookBack)
// Azure Portal only
| where AppDisplayName =~ "Azure Portal")
;
let successPortalSignins = azPortalSignins
| where TimeGenerated >= ago(timeRange)
// Azure Portal only and exclude non-failure Result Types
| where ResultType in ("0", "50125", "50140")
// Tagging identities not resolved to friendly names
//| extend Unresolved = iff(Identity matches regex isGUID, true, false)
| distinct TimeGenerated, UserPrincipalName
;
let failPortalSignins = azPortalSignins
| where TimeGenerated >= ago(timeRange)
// Azure Portal only and exclude non-failure Result Types
| where ResultType !in ("0", "50125", "50140", "70044", "70043")
// Tagging identities not resolved to friendly names
| extend Unresolved = iff(Identity matches regex isGUID, true, false)
;
// Verify there is no success for the same connection attempt after the fail
let failnoSuccess = failPortalSignins | join kind= leftouter (
successPortalSignins
) on UserPrincipalName
| where TimeGenerated > TimeGenerated1 or isempty(TimeGenerated1)
| project-away TimeGenerated1, UserPrincipalName1
;
// Lookup up resolved identities from last 7 days
let identityLookup = azPortalSignins
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
// Join resolved names to unresolved list from portal signins
let unresolvedNames = failnoSuccess | where Unresolved == true | join kind= inner (
identityLookup
) on UserId
| extend UserDisplayName = lu_UserDisplayName, UserPrincipalName = lu_UserPrincipalName
| project-away lu_UserDisplayName, lu_UserPrincipalName;
// Join Signins that had resolved names with list of unresolved that now have a resolved name
let u_azPortalSignins = failnoSuccess | where Unresolved == false | union unresolvedNames;
u_azPortalSignins
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
| extend Status = strcat(ResultType, ": ", ResultDescription), OS = tostring(DeviceDetail.operatingSystem), Browser = tostring(DeviceDetail.browser)
| extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
| extend FullLocation = strcat(Region,'|', State, '|', City)
| summarize TimeGenerated = make_list(TimeGenerated,100), Status = make_list(Status,100), IPAddresses = make_list(IPAddress,100), IPAddressCount = dcount(IPAddress), FailedLogonCount = count()
by UserPrincipalName, UserId, UserDisplayName, AppDisplayName, Browser, OS, FullLocation, Type
| mvexpand TimeGenerated, IPAddresses, Status
| extend TimeGenerated = todatetime(tostring(TimeGenerated)), IPAddress = tostring(IPAddresses), Status = tostring(Status)
| project-away IPAddresses
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, UserId, UserDisplayName, Status, FailedLogonCount, IPAddress, IPAddressCount, AppDisplayName, Browser, OS, FullLocation, Type
| where (IPAddressCount >= threshold_IPAddressCount and FailedLogonCount >= threshold_Failed) or FailedLogonCount >= threshold_FailedwithSingleIP
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.7
kind: Scheduled
Stages and Predicates
Parameters
let timeRange = 1d;
let lookBack = 7d;
let threshold_Failed = 5;
let threshold_FailedwithSingleIP = 20;
let threshold_IPAddressCount = 2;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let failPortalSignins, let failnoSuccess, let unresolvedNames and let u_azPortalSignins are inlined into the numbered stages below.
Let Binding: aadFunc
let aadFunc = (tableName:string){
let azPortalSignins = materialize(table(tableName)
| where TimeGenerated >= ago(lookBack)
| where AppDisplayName =~ "Azure Portal");
Let Binding: successPortalSignins
let successPortalSignins = azPortalSignins
| where TimeGenerated >= ago(timeRange)
| where ResultType in ("0", "50125", "50140")
| distinct TimeGenerated, UserPrincipalName;
Let Binding: identityLookup
let identityLookup = azPortalSignins
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
Stage 1: union
union of 2 branches
Stage 2: source
aadFunc(...)
Stage 3: source
aadFunc(...)
Failed Logins from Unknown or Invalid User
#This rule alerts when an unknown or invalid user generates more than 15 failed Okta management console login attempts from the same IP address within 5 minutes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Okta | System Log event type user.session.start: User logged in to Okta |
Rules Detecting the Same Action
These rules filter on the same operation.
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
- Okta Login From CrowdStrike Unmanaged Device (Panther)
Rule Body
id: 884be6e7-e568-418e-9c12-89229865ffde
name: Failed Logins from Unknown or Invalid User
description: |
This rule alerts when an unknown or invalid user generates more than 15 failed Okta management console login attempts from the same IP address within 5 minutes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: OktaSSO
dataTypes:
- Okta_CL
- connectorId: OktaSSOv2
dataTypes:
- OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110.001
query: |
// Adjust threshold for failed logins to reduce noise. This can be customized based on the organization's typical login patterns.
let FailureThreshold = 15;
// Adjust list of allowed users to reduce false positives. This can include service accounts, known test accounts, or any other accounts that are expected to generate failed login attempts.
let AllowedUsers = dynamic([]);
let FailedLogins = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| where isnotempty(actor_alternateId_s) and actor_alternateId_s !in (AllowedUsers)
| summarize count() by actor_alternateId_s, client_ipAddress_s, bin(TimeGenerated, 5m)
| where count_ > FailureThreshold
| project client_ipAddress_s, actor_alternateId_s;
OktaSSO
| join kind=inner (FailedLogins) on client_ipAddress_s, actor_alternateId_s
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| summarize FailedLoginCount = count(), City = take_any(column_ifexists('client_geographicalContext_city_s', "")), Country = take_any(column_ifexists('client_geographicalContext_country_s', "")), LastSeen = max(column_ifexists('TimeGenerated', now())) by actor_alternateId_s, client_ipAddress_s
| sort by LastSeen desc
| extend AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: client_ipAddress_s
customDetails:
FailedLoginCount: FailedLoginCount
City: City
Country: Country
alertDetailsOverride:
alertDisplayNameFormat: 'Failed logins from unknown or invalid user: {{actor_alternateId_s}} from IP {{client_ipAddress_s}}'
alertDescriptionFormat: 'Account {{actor_alternateId_s}} generated {{FailedLoginCount}} failed logins from IP {{client_ipAddress_s}} in the last 5 minutes.'
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let FailureThreshold = 15;
let AllowedUsers = dynamic([]);
Let Binding: FailedLogins
let FailedLogins = OktaSSO
| where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
| where isnotempty(actor_alternateId_s) and actor_alternateId_s !in (AllowedUsers)
| summarize count() by actor_alternateId_s, client_ipAddress_s, bin(TimeGenerated, 5m)
| where count_ > FailureThreshold
| project client_ipAddress_s, actor_alternateId_s;
Stage 1: source
OktaSSO
Stage 2: join
| join kind=inner (FailedLogins) on client_ipAddress_s, actor_alternateId_s
Stage 3: where
where eventType_s =~ "user.session.start" and outcome_reason_s =~ "VERIFICATION_ERROR"
Stage 4: summarize
summarize City, Country, FailedLoginCount, LastSeen by actor_alternateId_s, client_ipAddress_s
Stage 5: sort
sort by LastSeen
Stage 6: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
actor_alternateId_s | is_not_null | field:"actor_alternateId_s" kind:is_not_null | |
count_ | gt |
| field:"count_" kind:gt value:"15" |
eventType_s | eq |
| field:"eventType_s" kind:eq |
outcome_reason_s | eq |
| field:"outcome_reason_s" kind:eq |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
actor_alternateId_s | eq | [] | excludes:actor_alternateId_s field:"actor_alternateId_s" value:"[]" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
City | summarize |
Country | summarize |
FailedLoginCount | summarize |
LastSeen | summarize |
actor_alternateId_s | summarize |
client_ipAddress_s | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
Failed logon attempts by valid accounts within 10 mins
#Identifies when failed logon attempts are 20 or higher during a 10 minute period (2 failed logons per minute minimum) from valid account.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4625: An account failed to log on. |
Rule Body
id: 0777f138-e5d8-4eab-bec1-e11ddfbc2be2
name: Failed logon attempts by valid accounts within 10 mins
description: |
'Identifies when failed logon attempts are 20 or higher during a 10 minute period (2 failed logons per minute minimum) from valid account.'
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let threshold = 20;
let ReasontoSubStatus = datatable(SubStatus: string, Reason: string) [
"0xc000005e", "There are currently no logon servers available to service the logon request.",
"0xc0000064", "User logon with misspelled or bad user account",
"0xc000006a", "User logon with misspelled or bad password",
"0xc000006d", "Bad user name or password",
"0xc000006e", "Unknown user name or bad password",
"0xc000006f", "User logon outside authorized hours",
"0xc0000070", "User logon from unauthorized workstation",
"0xc0000071", "User logon with expired password",
"0xc0000072", "User logon to account disabled by administrator",
"0xc00000dc", "Indicates the Sam Server was in the wrong state to perform the desired operation",
"0xc0000133", "Clocks between DC and other computer too far out of sync",
"0xc000015b", "The user has not been granted the requested logon type (aka logon right) at this machine",
"0xc000018c", "The logon request failed because the trust relationship between the primary domain and the trusted domain failed",
"0xc0000192", "An attempt was made to logon, but the Netlogon service was not started",
"0xc0000193", "User logon with expired account",
"0xc0000224", "User is required to change password at next logon",
"0xc0000225", "Evidently a bug in Windows and not a risk",
"0xc0000234", "User logon with account locked",
"0xc00002ee", "Failure Reason: An Error occurred during Logon",
"0xc0000413", "Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine"
];
(union isfuzzy=true
(SecurityEvent
| where EventID == 4625
| where AccountType =~ "User"
| where SubStatus !~ '0xc0000064' and Account !in ('\\', '-\\-')
// SubStatus '0xc0000064' signifies 'Account name does not exist'
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", SourceComputerId),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, Account, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
),
(
(WindowsEvent
| where EventID == 4625 and not(EventData has '0xc0000064')
| extend TargetAccount = strcat(tostring(EventData.TargetDomainName), "\\", tostring(EventData.TargetUserName))
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(EventData.TargetUserName endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| where AccountType =~ "User"
| extend SubStatus = tostring(EventData.SubStatus)
| where SubStatus !~ '0xc0000064' and TargetAccount !in ('\\', '-\\-')
// SubStatus '0xc0000064' signifies 'Account name does not exist'
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", ""),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| extend Activity="4625 - An account failed to log on."
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend TargetDomainName = tostring(EventData.TargetDomainName)
| extend LogonType = tostring(EventData.LogonType)
| extend Status= tostring(EventData.Status)
| extend LogonProcessName = tostring(EventData.LogonProcessName)
| extend WorkstationName = tostring(EventData.WorkstationName)
| extend IpAddress = tostring(EventData.IpAddress)
| extend LogonTypeName=case(
LogonType == 2, "2 - Interactive",
LogonType == 3, "3 - Network",
LogonType == 4, "4 - Batch",
LogonType == 5, "5 - Service",
LogonType == 7, "7 - Unlock",
LogonType == 8, "8 - NetworkCleartext",
LogonType == 9, "9 - NewCredentials",
LogonType == 10, "10 - RemoteInteractive",
LogonType == 11, "11 - CachedInteractive",
tostring(LogonType)
)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
)))
| summarize arg_max(TimeGenerated, *) by Computer, TargetAccount, TargetUserName, TargetDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetAccount
- identifier: Name
columnName: TargetUserName
- identifier: NTDomain
columnName: TargetDomainName
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.2.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let threshold = 20;
Let Binding: ReasontoSubStatus
let ReasontoSubStatus = datatable(SubStatus: string, Reason: string) [
"0xc000005e", "There are currently no logon servers available to service the logon request.",
"0xc0000064", "User logon with misspelled or bad user account",
"0xc000006a", "User logon with misspelled or bad password",
"0xc000006d", "Bad user name or password",
"0xc000006e", "Unknown user name or bad password",
"0xc000006f", "User logon outside authorized hours",
"0xc0000070", "User logon from unauthorized workstation",
"0xc0000071", "User logon with expired password",
"0xc0000072", "User logon to account disabled by administrator",
"0xc00000dc", "Indicates the Sam Server was in the wrong state to perform the desired operation",
"0xc0000133", "Clocks between DC and other computer too far out of sync",
"0xc000015b", "The user has not been granted the requested logon type (aka logon right) at this machine",
"0xc000018c", "The logon request failed because the trust relationship between the primary domain and the trusted domain failed",
"0xc0000192", "An attempt was made to logon, but the Netlogon service was not started",
"0xc0000193", "User logon with expired account",
"0xc0000224", "User is required to change password at next logon",
"0xc0000225", "Evidently a bug in Windows and not a risk",
"0xc0000234", "User logon with account locked",
"0xc00002ee", "Failure Reason: An Error occurred during Logon",
"0xc0000413", "Logon Failure: The machine you are logging onto is protected by an authentication firewall. The specified account is not allowed to authenticate to the machine"
];
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 == 4625
| where AccountType =~ "User"
| where SubStatus !~ '0xc0000064' and Account !in ('\\', '-\\-')
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", SourceComputerId),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, Account, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
Leg 2: WindowsEvent
(WindowsEvent
| where EventID == 4625 and not(EventData has '0xc0000064')
| extend TargetAccount = strcat(tostring(EventData.TargetDomainName), "\\", tostring(EventData.TargetUserName))
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(EventData.TargetUserName endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| where AccountType =~ "User"
| extend SubStatus = tostring(EventData.SubStatus)
| where SubStatus !~ '0xc0000064' and TargetAccount !in ('\\', '-\\-')
| extend
ResourceId = column_ifexists("_ResourceId", _ResourceId),
SourceComputerId = column_ifexists("SourceComputerId", ""),
SubStatus = tolower(SubStatus)
| lookup ReasontoSubStatus on SubStatus
| extend coalesce(Reason, strcat('Unknown reason substatus: ', SubStatus))
| extend Activity="4625 - An account failed to log on."
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend TargetDomainName = tostring(EventData.TargetDomainName)
| extend LogonType = tostring(EventData.LogonType)
| extend Status= tostring(EventData.Status)
| extend LogonProcessName = tostring(EventData.LogonProcessName)
| extend WorkstationName = tostring(EventData.WorkstationName)
| extend IpAddress = tostring(EventData.IpAddress)
| extend LogonTypeName=case(
LogonType == 2, "2 - Interactive",
LogonType == 3, "3 - Network",
LogonType == 4, "4 - Batch",
LogonType == 5, "5 - Service",
LogonType == 7, "7 - Unlock",
LogonType == 8, "8 - NetworkCleartext",
LogonType == 9, "9 - NewCredentials",
LogonType == 10, "10 - RemoteInteractive",
LogonType == 11, "11 - CachedInteractive",
tostring(LogonType)
)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), FailedLogonCount = count() by bin(TimeGenerated,10m), EventID,
Activity, Computer, TargetAccount, TargetUserName, TargetDomainName,
LogonType, LogonTypeName, LogonProcessName, Status, SubStatus, Reason, ResourceId, SourceComputerId, WorkstationName, IpAddress
| where FailedLogonCount >= threshold
)
Applied to the Combined Result
| summarize arg_max(TimeGenerated, *) by Computer, TargetAccount, TargetUserName, TargetDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountType | eq |
| field:"AccountType" kind:eq value:"User" |
EventID | eq |
| field:"EventID" kind:eq value:"4625" |
FailedLogonCount | ge |
| field:"FailedLogonCount" kind:ge value:"20" |
SubStatus | ne |
| field:"SubStatus" kind:ne value:"0xc0000064" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Account | in | -\-, \ | excludes:Account field:"Account" value:"-\-" field:"Account" value:"\" |
EventData | match | 0xc0000064 | excludes:EventData field:"EventData" value:"0xc0000064" |
TargetAccount | in | -\-, \ | excludes:TargetAccount field:"TargetAccount" value:"-\-" field:"TargetAccount" value:"\" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
TargetAccount | summarize |
TargetDomainName | summarize |
TargetUserName | summarize |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
Field Effect MDR Alert: ARO Alert
#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.
Creates an incident for each Field Effect MDR ARO alert ingested into the workspace.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Defense Impairment |
Rule Body
id: 6d2d6b3f-7d7b-4d4a-9b2b-9f7f3b8c2a11
name: "Field Effect MDR Alert: ARO Alert"
description: "Creates an incident for each Field Effect MDR ARO alert ingested into the workspace."
severity: Medium
status: Available
tactics:
- Execution
- DefenseEvasion
relevantTechniques:
- T1059
- T1562
requiredDataConnectors:
- connectorId: FieldEffectCCF
dataTypes:
- FieldEffectAROAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 1h
suppressionEnabled: false
query: |
FieldEffectAROAlerts_CL
| where TimeGenerated >= ago(5m)
| project
TimeGenerated,
ARO = ID,
TITLE = Title,
DETAILS = DetailsMarkdown,
URL = PortalUrl,
SEVERITY = Severity,
Hostname,
IPAddress,
LastUser,
PRODUCT_NAME = "Field Effect MDR"
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: "Field Effect MDR Alert: ARO-{{ARO}}: {{TITLE}}"
alertDescriptionFormat: "{{DETAILS}}\n\nFor full information, visit: {{URL}}\n"
alertSeverityColumnName: SEVERITY
alertDynamicProperties:
- alertProperty: ProductName
value: PRODUCT_NAME
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: Account
fieldMappings:
- identifier: Name
columnName: LastUser
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
FieldEffectAROAlerts_CL
Stage 2: where
| where TimeGenerated >= ago(5m)
Stage 3: project
| project
TimeGenerated,
ARO = ID,
TITLE = Title,
DETAILS = DetailsMarkdown,
URL = PortalUrl,
SEVERITY = Severity,
Hostname,
IPAddress,
LastUser,
PRODUCT_NAME = "Field Effect MDR"
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ARO | project |
DETAILS | project |
Hostname | project |
IPAddress | project |
LastUser | project |
PRODUCT_NAME | project |
SEVERITY | project |
TITLE | project |
TimeGenerated | project |
URL | project |
Flare marketplace results
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'This query searches for underground markets and shops where illicit goods and services are bought and sold.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 9265ae4d-6bb0-4c18-961d-f7aae67d1546
name: Flare marketplace results
description: |
'This query searches for underground markets and shops where illicit goods and services are bought and sold.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Flare
dataTypes:
- FireworkV2_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1593
query: |
FireworkV2_CL
| where notempty(uid) and RiskScore >= 3
| extend index_name = split(uid, "/")[0]
| where index_name == "listing"
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
FireworkV2_CL
Stage 2: where
| where notempty(uid) and RiskScore >= 3
Stage 3: extend
| extend index_name = split(uid, "/")[0]
Stage 4: where
| where index_name == "listing"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RiskScore | ge |
| field:"RiskScore" kind:ge value:"3" |
index_name | eq |
| field:"index_name" kind:eq value:"listing" |
uid | is_not_null | field:"uid" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
index_name | extend |
Four-Eyes Authorization Request Created
#Detects when a four-eyes authorization request is created.
Rule Body
id: fe6226cd-fee7-4d0d-88d2-160311207b57
name: Four-Eyes Authorization Request Created
description: Detects when a four-eyes authorization request is created.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 42402\n| extend Operation =\
\ extract(\"Operation=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n [\"Operation Name\"\
] = Operation,\n MessageDetails = Description,\n Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 42402
Stage 3: extend
| extend Operation = extract("Operation=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
["Operation Name"] = Operation,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"42402" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Operation Name | project |
Severity | project |
User Name | project |
Illegal Function Codes for ICS traffic (Microsoft Defender for IoT)
#'This alert leverages Defender for IoT to detect Illegal function codes in SCADA equipment indicating improper application configuration or malicious activity such using illegal values within a protocol to exploit a PLC vulnerability.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impair Process Control |
Rule Body
id: 70be4a31-9d2b-433b-bdc7-da8928988069
name: Illegal Function Codes for ICS traffic (Microsoft Defender for IoT)
description: |
'This alert leverages Defender for IoT to detect Illegal function codes in SCADA equipment indicating improper application configuration or malicious activity such using illegal values within a protocol to exploit a PLC vulnerability.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: IoT
dataTypes:
- SecurityAlert (ASC for IoT)
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- ImpairProcessControl
relevantTechniques:
- T0855
query: |
let alertList = dynamic(["Function Code Not Supported by Outstation", "Illegal BACNet message", "Illegal Connection Attempt on Port 0", "Illegal DNP3 Operation", "Illegal MODBUS Operation (Exception Raised by Master)", "Illegal MODBUS Operation (Function Code Zero)", "Incorrect Parameter Sent to Outstation", "Initiation of an Obsolete Function Code (Initialize Data)", "Initiation of an Obsolete Function Code (Save Config)", "Modbus Exception", "Unknown Object Sent to Outstation", "Usage of a Reserved Function Code", "Usage of Improper Formatting by Outstation", "Usage of Reserved Status Flags (IIN)", "Unauthorized communication was detected by a User Defined Protocol Rule", "Unauthorized Operation was detected by a User Defined whitelist Alert", "Illegal Protocol Version", "New Activity Detected - LonTalk Network Variable", "New Activity Detected - Ovation Data Request", "New Activity Detected - Read/Write Command (AMS Index Group)", "New Activity Detected - Read/Write Command (AMS Index Offset)", "New Activity Detected - Unauthorized DeltaV Message Type", "New Activity Detected - Unauthorized DeltaV ROC Operation", "New Activity Detected - Using AMS Protocol Command", "New Activity Detected - Using Siemens SICAM Command", "New Activity Detected - Using Suitelink Protocol command", "New Activity Detected - Using Suitelink Protocol sessions", "New Activity Detected - Using Yokogawa VNetIP Command", "Omron FINS Unauthorized Command", "Toshiba Computer Link Unauthorized Command", "Unauthorized ABB Totalflow File Operation", "Unauthorized ABB Totalflow Register Operation", "Unauthorized Access to Siemens S7 Plus Object", "Unauthorized BACNet Object Access", "Unauthorized BACNet Route", "Unauthorized Emerson ROC Operation", "Unauthorized GE SRTP File Access", "Unauthorized GE SRTP Protocol Command", "Unauthorized GE SRTP System Memory Operation", "Unauthorized Mitsubishi MELSEC Command", "Unauthorized MMS Service", "Unauthorized OPC UA Activity", "Unauthorized OPC UA Request/Response", "Unauthorized Profinet Frame Type", "Unauthorized SAIA S-Bus Command", "Unauthorized Siemens S7 Execution of Control Function", "Unauthorized Siemens S7 Execution of User Defined Function", "Unauthorized Siemens S7 Plus Block Access", "Unauthorized Siemens S7 Plus Operation", "Unauthorized SNMP Operation", "Unpermitted Modbus Schneider Electric Extension", "Unpermitted Usage of ASDU Types", "Unpermitted Usage of DNP3 Function Code", "Unpermitted Usage of Modbus Function Code", "Unauthorized Operation was detected by a User Defined Rule", "Unauthorized PLC Configuration Read", "Unauthorized PLC Programming", "Unauthorized PLC Configuration Write", "Unauthorized PLC Program Upload", "Slave Device Received Illegal"]);
SecurityAlert
| where ProviderName == "IoTSecurity"
| where AlertName has_any (alertList)
| extend ExtendedProperties = parse_json(ExtendedProperties)
| where tostring(ExtendedProperties.isNew) == "True"
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
entityMappings:
sentinelEntitiesMappings:
- columnName: Entities
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
Sensor: DeviceId
Protocol: Protocol
VendorOriginalId: VendorOriginalId
AlertManagementUri: AlertManagementUri
alertDetailsOverride:
alertDisplayNameFormat: (MDIoT) {{AlertName}}
alertDescriptionFormat: (MDIoT) {{Description}}
alertTacticsColumnName: Tactics
alertSeverityColumnName: AlertSeverity
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: RemediationSteps
value: RemediationSteps
- alertProperty: Techniques
value: Techniques
- alertProperty: ProductComponentName
value: ProductComponentName
- alertProperty: AlertLink
value: AlertLink
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: alertList
let alertList = dynamic(["Function Code Not Supported by Outstation", "Illegal BACNet message", "Illegal Connection Attempt on Port 0", "Illegal DNP3 Operation", "Illegal MODBUS Operation (Exception Raised by Master)", "Illegal MODBUS Operation (Function Code Zero)", "Incorrect Parameter Sent to Outstation", "Initiation of an Obsolete Function Code (Initialize Data)", "Initiation of an Obsolete Function Code (Save Config)", "Modbus Exception", "Unknown Object Sent to Outstation", "Usage of a Reserved Function Code", "Usage of Improper Formatting by Outstation", "Usage of Reserved Status Flags (IIN)", "Unauthorized communication was detected by a User Defined Protocol Rule", "Unauthorized Operation was detected by a User Defined whitelist Alert", "Illegal Protocol Version", "New Activity Detected - LonTalk Network Variable", "New Activity Detected - Ovation Data Request", "New Activity Detected - Read/Write Command (AMS Index Group)", "New Activity Detected - Read/Write Command (AMS Index Offset)", "New Activity Detected - Unauthorized DeltaV Message Type", "New Activity Detected - Unauthorized DeltaV ROC Operation", "New Activity Detected - Using AMS Protocol Command", "New Activity Detected - Using Siemens SICAM Command", "New Activity Detected - Using Suitelink Protocol command", "New Activity Detected - Using Suitelink Protocol sessions", "New Activity Detected - Using Yokogawa VNetIP Command", "Omron FINS Unauthorized Command", "Toshiba Computer Link Unauthorized Command", "Unauthorized ABB Totalflow File Operation", "Unauthorized ABB Totalflow Register Operation", "Unauthorized Access to Siemens S7 Plus Object", "Unauthorized BACNet Object Access", "Unauthorized BACNet Route", "Unauthorized Emerson ROC Operation", "Unauthorized GE SRTP File Access", "Unauthorized GE SRTP Protocol Command", "Unauthorized GE SRTP System Memory Operation", "Unauthorized Mitsubishi MELSEC Command", "Unauthorized MMS Service", "Unauthorized OPC UA Activity", "Unauthorized OPC UA Request/Response", "Unauthorized Profinet Frame Type", "Unauthorized SAIA S-Bus Command", "Unauthorized Siemens S7 Execution of Control Function", "Unauthorized Siemens S7 Execution of User Defined Function", "Unauthorized Siemens S7 Plus Block Access", "Unauthorized Siemens S7 Plus Operation", "Unauthorized SNMP Operation", "Unpermitted Modbus Schneider Electric Extension", "Unpermitted Usage of ASDU Types", "Unpermitted Usage of DNP3 Function Code", "Unpermitted Usage of Modbus Function Code", "Unauthorized Operation was detected by a User Defined Rule", "Unauthorized PLC Configuration Read", "Unauthorized PLC Programming", "Unauthorized PLC Configuration Write", "Unauthorized PLC Program Upload", "Slave Device Received Illegal"]);
Stage 1: source
SecurityAlert
Stage 2: where
| where ProviderName == "IoTSecurity"
Stage 3: where
| where AlertName has_any (alertList)
Stage 4: extend
| extend ExtendedProperties = parse_json(ExtendedProperties)
Stage 5: where
| where tostring(ExtendedProperties.isNew) == "True"
Stage 6: extend
| extend DeviceId = tostring(ExtendedProperties.DeviceId),
SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress),
DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress),
RemediationSteps = tostring(parse_json(RemediationSteps)[0]),
Protocol = tostring(ExtendedProperties.Protocol),
AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
Stage 7: project
| project
TimeGenerated,
DeviceId,
ProductName,
ProductComponentName,
AlertSeverity,
AlertName,
Description,
Protocol,
SourceDeviceAddress,
DestDeviceAddress,
RemediationSteps,
Tactics,
Entities,
VendorOriginalId,
AlertLink,
AlertManagementUri,
Techniques
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertName | match |
| field:"AlertName" kind:match |
ExtendedProperties.isNew | eq |
| field:"ExtendedProperties.isNew" kind:eq value:"True" |
ProviderName | eq |
| field:"ProviderName" kind:eq value:"IoTSecurity" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertLink | project |
AlertManagementUri | project |
AlertName | project |
AlertSeverity | project |
Description | project |
DestDeviceAddress | project |
DeviceId | project |
Entities | project |
ProductComponentName | project |
ProductName | project |
Protocol | project |
RemediationSteps | project |
SourceDeviceAddress | project |
Tactics | project |
Techniques | project |
TimeGenerated | project |
VendorOriginalId | project |
Local Admin Group Changes
#This query searches for changes to the local administrators group. Blogpost: https://www.verboon.info/2020/09/hunting-for-local-group-membership-changes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
Rule Body
id: 63aa43c2-e88e-4102-aea5-0432851c541a
name: Local Admin Group Changes
description: |
This query searches for changes to the local administrators group.
Blogpost: https://www.verboon.info/2020/09/hunting-for-local-group-membership-changes.
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- IdentityInfo
- DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |
let machineAccountSIDs = dynamic([
"S-1-5-18",
"S-1-5-20",
"S-1-5-19"]);
let ADAZUsers = IdentityInfo
| extend DirectoryDomain = AccountDomain
| extend DirectoryAccount = AccountName
| extend OnPremSid = AccountSID
| distinct DirectoryDomain , DirectoryAccount , OnPremSid , AccountCloudSID, AccountUPN, GivenName, Surname;
// check for any new created or modified local accounts
let NewUsers = DeviceEvents
| where ActionType contains "UserAccountCreated" or ActionType contains "UserAccountModified"
| extend lUserAdded = AccountName
| extend NewUserSID = AccountSid
| extend laccountdomain = AccountDomain
| distinct NewUserSID, lUserAdded,laccountdomain;
// Check for any local group changes and enrich the data with the account name obtained from the previous query
DeviceEvents
| where ActionType == 'UserAccountAddedToLocalGroup'
// Exclude machine and wellknown SIDs
| where (AccountSid !in (machineAccountSIDs)) and (AccountSid matches regex @"S-\d-\d+-\d+-(\d+-){1,5}\d+")
| extend LocalGroupSID = tostring(parse_json(AdditionalFields).GroupSid)
| extend LocalGroup = tostring(parse_json(AdditionalFields).GroupName)
| extend AddedAccountSID = AccountSid
| extend Actor = trim(@"[^\w]+",InitiatingProcessAccountName)
// limit to local administrators group
// | where LocalGroupSID contains "S-1-5-32-544"
| join kind=leftouter (NewUsers)
on $left.AddedAccountSID == $right.NewUserSID
| project TimeGenerated, DeviceName, LocalGroup,LocalGroupSID, AddedAccountSID, lUserAdded , Actor, ActionType , laccountdomain
| join kind=innerunique (ADAZUsers)
on $left.AddedAccountSID == $right.OnPremSid
| extend UserAdded = iff(isnotempty(lUserAdded),strcat(laccountdomain,"\\", lUserAdded), strcat(DirectoryDomain,"\\", DirectoryAccount))
| extend AccountName = iff(isnotempty(lUserAdded), lUserAdded, DirectoryAccount)
| project TimeGenerated, DeviceName, LocalGroup, LocalGroupSID, AddedAccountSID, UserAdded ,Actor, ActionType, AccountName, laccountdomain
| where DeviceName !contains Actor
// Provide details on actors that added users
// | summarize count() by Actor
// | join ADAZUsers
// on $left.Actor == $right.DirectoryAccount
// | render piechart
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserAdded
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: laccountdomain
version: 1.0.2
kind: Scheduled
Stages and Predicates
Let Binding: machineAccountSIDs
let machineAccountSIDs = dynamic([
"S-1-5-18",
"S-1-5-20",
"S-1-5-19"]);
Let Binding: ADAZUsers
let ADAZUsers = IdentityInfo
| extend DirectoryDomain = AccountDomain
| extend DirectoryAccount = AccountName
| extend OnPremSid = AccountSID
| distinct DirectoryDomain , DirectoryAccount , OnPremSid , AccountCloudSID, AccountUPN, GivenName, Surname;
Let Binding: NewUsers
let NewUsers = DeviceEvents
| where ActionType contains "UserAccountCreated" or ActionType contains "UserAccountModified"
| extend lUserAdded = AccountName
| extend NewUserSID = AccountSid
| extend laccountdomain = AccountDomain
| distinct NewUserSID, lUserAdded,laccountdomain;
Stage 1: source
let ADAZUsers
Stage 2: source
let NewUsers
Stage 3: source
DeviceEvents
Stage 4: where
where ActionType == "UserAccountAddedToLocalGroup"
Stage 5: where
where not (AccountSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20")) and AccountSid matches regex @"S-\d-\d+-\d+-(\d+-){1,5}\d+"
Stage 6: extend (4 consecutive steps)
extend Actor, AddedAccountSID, LocalGroup, LocalGroupSID
Stage 7: join
join kind=leftouter (NewUsers) on $left.AddedAccountSID, $right.NewUserSID
Stage 8: project
project ActionType, Actor, AddedAccountSID, DeviceName, LocalGroup, LocalGroupSID, TimeGenerated, lUserAdded, laccountdomain
Stage 9: join
join kind=innerunique (ADAZUsers) on $left.AddedAccountSID, $right.OnPremSid
Stage 10: extend
extend UserAdded
UserAdded =isnotempty(lUserAdded)strcat(laccountdomain, "\\", lUserAdded)strcat(DirectoryDomain, "\\", DirectoryAccount)Stage 11: extend
extend AccountName
AccountName =isnotempty(lUserAdded)lUserAddedDirectoryAccountStage 12: project
project AccountName, ActionType, Actor, AddedAccountSID, DeviceName, LocalGroup, LocalGroupSID, TimeGenerated, UserAdded, laccountdomain
Stage 13: where
where DeviceName !contains Actor
Stage 14: extend
extend HostName
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 15: extend
extend DnsDomain
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountSid | regex_match |
| field:"AccountSid" kind:regex_match value:"S-\d-\d+-\d+-(\d+-){1,5}\d+" |
ActionType | contains |
| field:"ActionType" kind:contains |
ActionType | eq |
| field:"ActionType" kind:eq value:"UserAccountAddedToLocalGroup" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AccountSid | in | S-1-5-18, S-1-5-19, S-1-5-20 | excludes:AccountSid field:"AccountSid" value:"S-1-5-18" field:"AccountSid" value:"S-1-5-19" field:"AccountSid" value:"S-1-5-20" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ActionType | project |
Actor | project |
AddedAccountSID | project |
DeviceName | project |
LocalGroup | project |
LocalGroupSID | project |
TimeGenerated | project |
UserAdded | project |
laccountdomain | project |
HostName | extend |
DnsDomain | extend |
Malformed user agent
#Malware authors will sometimes hardcode user agent string values when writing the network communication component of their malware. Malformed user agents can be an indication of such malware.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Command & Control |
Telemetry Coverage
Rule Body
id: a357535e-f722-4afe-b375-cff362b2b376
name: Malformed user agent
description: |
'Malware authors will sometimes hardcode user agent string values when writing the network communication component of their malware. Malformed user agents can be an indication of such malware.'
severity: Medium
requiredDataConnectors:
- connectorId: WAF
dataTypes:
- AzureDiagnostics
- connectorId: Office365
dataTypes:
- OfficeActivity
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AzureMonitor(IIS)
dataTypes:
- W3CIISLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
- Execution
relevantTechniques:
- T1189
- T1071
- T1203
query: |
(union isfuzzy=true
(OfficeActivity | where UserAgent != ""),
(OfficeActivity
| where RecordType in ("AzureActiveDirectory", "AzureActiveDirectoryStsLogon")
| extend OperationName = Operation
| parse ExtendedProperties with * 'User-Agent\\":\\"' UserAgent2 '\\' *
| parse ExtendedProperties with * 'UserAgent", "Value": "' UserAgent1 '"' *
| where isnotempty(UserAgent1) or isnotempty(UserAgent2)
| extend UserAgent = iff( RecordType == 'AzureActiveDirectoryStsLogon', UserAgent1, UserAgent2)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, Account = UserId, Type, RecordType, Operation
),
(AzureDiagnostics
| where ResourceType =~ "APPLICATIONGATEWAYS"
| where OperationName =~ "ApplicationGatewayAccess"
| extend ClientIP = columnifexists("clientIP_s", "None"), UserAgent = columnifexists("userAgent_s", "None")
| where UserAgent != '-'
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, requestUri_s, httpMethod_s, host_s, requestQuery_s, Type
),
(
W3CIISLog
| where isnotempty(csUserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent = csUserAgent, SourceIP = cIP, Account = csUserName, Type, sSiteName, csMethod, csUriStem
),
(
AWSCloudTrail
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = SourceIpAddress, Account = UserIdentityUserName, Type, EventSource, EventName
),
(SigninLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
),
(AADNonInteractiveUserSignInLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
)
)
// Likely artefact of hardcoding
| where UserAgent startswith "User" or UserAgent startswith '\"'
// Incorrect casing
or (UserAgent startswith "Mozilla" and not(UserAgent contains_cs "Mozilla"))
// Incorrect casing
or UserAgent contains_cs "(Compatible;"
// Missing MSIE version
or UserAgent matches regex @"MSIE\s?;"
// Incorrect spacing around MSIE version
or UserAgent matches regex @"MSIE(?:\d|.{1,5}?\d\s;)"
| extend AccountName = split(Account, "@")[0], UPNSuffix = split(Account, "@")[1]
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
version: 1.0.6
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Union isfuzzy=true (7 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: OfficeActivity, OfficeActivity, AzureDiagnostics, W3CIISLog, AWSCloudTrail, SigninLogs, AADNonInteractiveUserSignInLogs
Leg 1: OfficeActivity
OfficeActivity | where UserAgent != ""
Leg 2: OfficeActivity
OfficeActivity
| where RecordType in ("AzureActiveDirectory", "AzureActiveDirectoryStsLogon")
| extend OperationName = Operation
| parse ExtendedProperties with * 'User-Agent\\":\\"' UserAgent2 '\\' *
| parse ExtendedProperties with * 'UserAgent", "Value": "' UserAgent1 '"' *
| where isnotempty(UserAgent1) or isnotempty(UserAgent2)
| extend UserAgent = iff( RecordType == 'AzureActiveDirectoryStsLogon', UserAgent1, UserAgent2)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, Account = UserId, Type, RecordType, Operation
Leg 3: AzureDiagnostics
AzureDiagnostics
| where ResourceType =~ "APPLICATIONGATEWAYS"
| where OperationName =~ "ApplicationGatewayAccess"
| extend ClientIP = columnifexists("clientIP_s", "None"), UserAgent = columnifexists("userAgent_s", "None")
| where UserAgent != '-'
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, requestUri_s, httpMethod_s, host_s, requestQuery_s, Type
Leg 4: W3CIISLog
W3CIISLog
| where isnotempty(csUserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent = csUserAgent, SourceIP = cIP, Account = csUserName, Type, sSiteName, csMethod, csUriStem
Leg 5: AWSCloudTrail
AWSCloudTrail
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = SourceIpAddress, Account = UserIdentityUserName, Type, EventSource, EventName
Leg 6: SigninLogs
SigninLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
Leg 7: AADNonInteractiveUserSignInLogs
AADNonInteractiveUserSignInLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
Applied to the Combined Result
| where UserAgent startswith "User" or UserAgent startswith '\"'
or (UserAgent startswith "Mozilla" and not(UserAgent contains_cs "Mozilla"))
or UserAgent contains_cs "(Compatible;"
or UserAgent matches regex @"MSIE\s?;"
or UserAgent matches regex @"MSIE(?:\d|.{1,5}?\d\s;)"
| extend AccountName = split(Account, "@")[0], UPNSuffix = split(Account, "@")[1]
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"ApplicationGatewayAccess" |
RecordType | in |
| field:"RecordType" kind:in |
ResourceType | eq |
| field:"ResourceType" kind:eq value:"APPLICATIONGATEWAYS" |
UserAgent | contains |
| field:"UserAgent" kind:contains value:"(Compatible;" |
UserAgent | is_not_null | field:"aws::userAgent" kind:is_not_null | |
UserAgent | ne |
| field:"UserAgent" kind:ne value:"-" |
UserAgent | regex_match |
| field:"UserAgent" kind:regex_match |
UserAgent | starts_with |
| field:"UserAgent" kind:starts_with |
UserAgent1 | is_not_null | field:"UserAgent1" kind:is_not_null | |
UserAgent2 | is_not_null | field:"UserAgent2" kind:is_not_null | |
csUserAgent | is_not_null | field:"csUserAgent" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | summarize |
AppDisplayName | summarize |
ClientAppUsed | summarize |
EndTime | summarize |
OperationName | summarize |
SourceIP | summarize |
StartTime | summarize |
Type | summarize |
UserAgent | summarize |
AccountName | extend |
UPNSuffix | extend |
McAfee ePO - Task error
#Detects when task error occurs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Defense Impairment |
Telemetry Coverage
Rule Body
id: 3e397e31-7964-417e-a3e0-0acfaa2056f4
name: McAfee ePO - Task error
description: |
'Detects when task error occurs.'
severity: Medium
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
- T1070
query: |
McAfeeEPOEvent
| where EventId in ('1003', '1067')
| extend EventMessage = case(EventId == '1003', 'Error starting Task',
'Unable to start scheduled task')
| project DvcIpAddr, EventId, EventMessage
| extend IPCustomEntity = DvcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
McAfeeEPOEvent
Stage 2: where
| where EventId in ('1003', '1067')
Stage 3: extend
| extend EventMessage = case(EventId == '1003', 'Error starting Task',
'Unable to start scheduled task')
EventMessage =EventId == 1003'Error starting Task''Unable to start scheduled task'Stage 4: project
| project DvcIpAddr, EventId, EventMessage
Stage 5: extend
| extend IPCustomEntity = DvcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventId | in |
| field:"EventId" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DvcIpAddr | project |
EventId | project |
EventMessage | project |
IPCustomEntity | extend |
OracleDBAudit - New user account
#'Detects when an action was made by new user.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Rule Body
id: cca7b348-e904-4a7a-8f26-d22d4d477119
name: OracleDBAudit - New user account
description: |
'Detects when an action was made by new user.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 3h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1078
query: |
let lbtime_d = 14d;
let lbtime_24h = 24h;
let known_users = OracleDatabaseAuditEvent
| where TimeGenerated between (ago(lbtime_d) .. ago(lbtime_24h))
| where isnotempty(DstUserName)
| where isnotempty(Action)
| summarize makeset(DstUserName);
OracleDatabaseAuditEvent
| where isnotempty(DstUserName)
| where isnotempty(Action)
| where DstUserName !in (known_users)
| project DstUserName
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let lbtime_d = 14d;
let lbtime_24h = 24h;
Let Binding: known_users
let known_users = OracleDatabaseAuditEvent
| where TimeGenerated between (ago(lbtime_d) .. ago(lbtime_24h))
| where isnotempty(DstUserName)
| where isnotempty(Action)
| summarize makeset(DstUserName);
Stage 1: source
OracleDatabaseAuditEvent
Stage 2: where
| where isnotempty(DstUserName)
Stage 3: where
| where isnotempty(Action)
Stage 4: where
| where DstUserName !in (known_users)
Stage 5: project
| project DstUserName
Stage 6: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | is_not_null | field:"Action" kind:is_not_null | |
DstUserName | is_not_null | field:"DstUserName" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstUserName | eq | known_users | excludes:DstUserName field:"DstUserName" value:"known_users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | project |
AccountCustomEntity | extend |
Power Platform - DLP policy updated or removed
#Identifies changes to DLP policy, specifically policies which are updated or removed.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | GovernanceApiPolicyOperation |
Rule Body
id: 1b2e6172-85c5-417a-90c3-7cc80cb787f5
kind: Scheduled
name: Power Platform - DLP policy updated or removed
description: Identifies changes to DLP policy, specifically policies which are updated
or removed.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: PowerPlatformAdmin
dataTypes:
- PowerPlatformAdminActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1480
query: |
let create_policy_ignore_time_window = 10m;
let query_frequency = 1h;
let dlp_policy_events = PowerPlatformAdminActivity
| where TimeGenerated >= ago(query_frequency)
| where EventOriginalType == "GovernanceApiPolicyOperation"
| where PropertyCollection has_any ("DeleteDlpPolicy", "UpdateDlpPolicy", "CreateDlpPolicy")
| mv-expand PropertyCollection
| extend
Name = tostring(PropertyCollection.Name),
Value = tostring(PropertyCollection.Value)
| summarize Properties = make_bag(bag_pack(Name, Value))
by
TimeGenerated,
EventOriginalUid
| extend
PolicyName = tostring(Properties['powerplatform.analytics.resource.display_name']),
EventType = tostring(Properties['powerplatform.analytics.resource.tenant.governance.api_policy.operation_name']),
ActorName = tostring(Properties['enduser.principal_name']),
PolicyId = tostring(Properties['powerplatform.analytics.resource.id']),
AdditionalInfo = Properties['powerplatform.analytics.resource.tenant.governance.api_policy.additional_resources'];
let delete_events = dlp_policy_events
| where EventType == "DeleteDlpPolicy";
let update_events = dlp_policy_events
| where EventType == "UpdateDlpPolicy";
let create_events = dlp_policy_events
| where EventType == "CreateDlpPolicy"
| extend ignore_time = TimeGenerated + create_policy_ignore_time_window;
union
delete_events,
(update_events
| join kind=leftouter (
create_events
| project-away TimeGenerated
)
on PolicyId
| where isempty(ignore_time) or TimeGenerated > ignore_time
| project-away ignore_time)
| where TimeGenerated >= ago(query_frequency)
| extend
AccountName = tostring(split(ActorName, "@")[0]),
UPNSuffix = tostring(split(ActorName, "@")[1])
| project
TimeGenerated,
ActorName,
EventType,
PolicyName,
PolicyId,
AccountName,
UPNSuffix,
AdditionalInfo
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
alertDetailsOverride:
alertDisplayNameFormat: PowerPlatform - DLP policy {{EventType}} event detected.
alertDescriptionFormat: A DLP policy {{PolicyName}} was as modfiied or deleted.
Event type {{EventType}}
customDetails:
Policy: PolicyId
PolicyName: PolicyName
version: 3.2.0
Stages and Predicates
Parameters
let create_policy_ignore_time_window = 10m;
let query_frequency = 1h;
let dlp_policy_events and let update_events are inlined into the numbered stages below.
Let Binding: delete_events
let delete_events = dlp_policy_events
| where EventType == "DeleteDlpPolicy";
Let Binding: create_events
let create_events = dlp_policy_events
| where EventType == "CreateDlpPolicy"
| extend ignore_time = TimeGenerated + create_policy_ignore_time_window;
Stage 1: union
union of 2 branches
Stage 2: source
PowerPlatformAdminActivity
Stage 3: where
where TimeGenerated >= ago(3600s)
Stage 4: where
where EventOriginalType == "GovernanceApiPolicyOperation"
Stage 5: where
where (PropertyCollection has "DeleteDlpPolicy" or PropertyCollection has "UpdateDlpPolicy" or PropertyCollection has "CreateDlpPolicy")
Stage 6: mv-expand
mv-expand PropertyCollection
Stage 7: extend
extend Name, Value
Stage 8: summarize
summarize Properties by TimeGenerated, EventOriginalUid
Stage 9: extend
extend ActorName, AdditionalInfo, EventType, PolicyId, PolicyName
Stage 10: where
where EventType == "DeleteDlpPolicy"
Stage 11: source
PowerPlatformAdminActivity
Stage 12: where
where TimeGenerated >= ago(3600s)
Stage 13: where
where EventOriginalType == "GovernanceApiPolicyOperation"
Stage 14: where
where (PropertyCollection has "DeleteDlpPolicy" or PropertyCollection has "UpdateDlpPolicy" or PropertyCollection has "CreateDlpPolicy")
Stage 15: mv-expand
mv-expand PropertyCollection
Stage 16: extend
extend Name, Value
Stage 17: summarize
summarize Properties by TimeGenerated, EventOriginalUid
Stage 18: extend
extend ActorName, AdditionalInfo, EventType, PolicyId, PolicyName
Stage 19: where
where EventType == "UpdateDlpPolicy"
Stage 20: join
join kind=leftouter (create_events) on PolicyId
Stage 21: where
where (TimeGenerated > ignore_time or isempty(ignore_time))
Stage 22: project-away
project-away ignore_time
Stage 23: where
where TimeGenerated >= ago(3600s)
Stage 24: extend
extend AccountName, UPNSuffix
Stage 25: project
project AccountName, ActorName, AdditionalInfo, EventType, PolicyId, PolicyName, TimeGenerated, UPNSuffix
Stage 26: summarize aggregation inside the join branch
summarize by TimeGenerated, EventOriginalUid
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventOriginalType | eq |
| field:"EventOriginalType" kind:eq value:"GovernanceApiPolicyOperation" |
EventType | eq |
| field:"EventType" kind:eq |
PropertyCollection | match |
| field:"PropertyCollection" kind:match |
ignore_time | is_null | field:"ignore_time" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventOriginalUid | summarize |
TimeGenerated | summarize |
PROD (TM012.1) - GPO - Enforced Outside of Tier Model
#Enforcing of a GPO could bypass or override Tier Model GPOs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Defense Impairment |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5136: A directory service object was modified. |
Rule Body
id: 05846968-15cc-4309-bddc-eaa296f84af1
name: 'PROD (TM012.1) - GPO - Enforced Outside of Tier Model'
description: |
Enforcing of a GPO could bypass or override Tier Model GPOs.
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1484.001
query: |
SecurityEvent
| where EventID == 5136
and not(EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
or EventData matches regex @"(?i)OU=Tier Model Administration"
or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
and Activity has "modified"
and EventData has '"ObjectClass">organizationalUnit'
and EventData has '<Data Name="AttributeLDAPDisplayName">gPLink</Data>'
and (EventData has ';0]'
or EventData has ';1]'
or EventData has ';2]')
and EventData has '"OperationType">%%14674'
| extend ObjectName = extract("<Data Name=\"ObjectDN\">((?i)OU=[^,]+(,OU=[^,]+)*),(?i)DC=", 1, EventData)
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend LastThreeChars = substring(AttributeValue, strlen(AttributeValue) - 2, 3)
| extend AttributeValueDescription = case(
LastThreeChars == ";0", "Linked",
LastThreeChars == ";1", "Unlinked",
LastThreeChars == ";2", "Enforced",
"Unknown")
| where AttributeValueDescription == "Enforced"
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend ObjectTier = "Outside Tier Model"
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, ActivityType = AttributeValueDescription
, AttributeLDAP
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ObjectName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
customDetails:
ObjectName: ObjectName
ObjectTier: ObjectTier
ObjectClass: ObjectClass
ActivityType: ActivityType
AttributeLDAP: AttributeLDAP
Account: Account
Domain: Domain
Computer: Computer
Channel: Channel
EventID: EventID
EventData: EventData
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: '(TM012.1) A GPO was enforced and is overwriting the Tier Model GPOs'
alertDescriptionFormat: 'Enforcing a GPO outside the Tier Model can overwrite and bypass Tier Model GPO with security controls.'
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5136
and not(EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
or EventData matches regex @"(?i)OU=Tier Model Administration"
or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
and Activity has "modified"
and EventData has '"ObjectClass">organizationalUnit'
and EventData has '<Data Name="AttributeLDAPDisplayName">gPLink</Data>'
and (EventData has ';0]'
or EventData has ';1]'
or EventData has ';2]')
and EventData has '"OperationType">%%14674'
Stage 3: extend (5 consecutive steps)
| extend ObjectName = extract("<Data Name=\"ObjectDN\">((?i)OU=[^,]+(,OU=[^,]+)*),(?i)DC=", 1, EventData)
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend LastThreeChars = substring(AttributeValue, strlen(AttributeValue) - 2, 3)
| extend AttributeValueDescription = case(
LastThreeChars == ";0", "Linked",
LastThreeChars == ";1", "Unlinked",
LastThreeChars == ";2", "Enforced",
"Unknown")
Stage 4: where
| where AttributeValueDescription == "Enforced"
Stage 5: extend (3 consecutive steps)
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend ObjectTier = "Outside Tier Model"
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
Stage 6: project
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectClass
, ActivityType = AttributeValueDescription
, AttributeLDAP
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Activity | match |
| field:"Activity" kind:match value:"modified" |
AttributeValueDescription | eq |
| field:"AttributeValueDescription" kind:eq value:"Enforced" |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"5136" |
Exclusions
The rule actively suppresses these predicates.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
AttributeLDAP | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
ObjectClass | project |
ObjectName | project |
ObjectTier | project |
TimeGenerated | project |
Security Event log cleared
#Checks for event id 1102 which indicates the security event log was cleared. It uses Event Source Name "Microsoft-Windows-Eventlog" to avoid generating false positives from other sources, like AD FS servers for instance.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Eventlog | Event ID 1102: The audit log was cleared. |
Rule Body
id: 80da0a8f-cfe1-4cd0-a895-8bc1771a720e
name: Security Event log cleared
description: |
'Checks for event id 1102 which indicates the security event log was cleared.
It uses Event Source Name "Microsoft-Windows-Eventlog" to avoid generating false positives from other sources, like AD FS servers for instance.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1070
query: |
(union isfuzzy=true
(
SecurityEvent
| where EventID == 1102 and EventSourceName =~ "Microsoft-Windows-Eventlog"
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), EventCount = count() by Computer, Account, EventID, Activity
),
(
WindowsEvent
| where EventID == 1102 and Provider =~ "Microsoft-Windows-Eventlog"
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend Activity= "1102 - The audit log was cleared."
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), EventCount = count() by Computer, Account, EventID, Activity
)
)
| extend Name=tostring(split(Account, "@")[0]), UPNSuffix=tostring(split(Account, "@")[1])
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.1.5
kind: Scheduled
Stages and Predicates
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 == 1102 and EventSourceName =~ "Microsoft-Windows-Eventlog"
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), EventCount = count() by Computer, Account, EventID, Activity
Leg 2: WindowsEvent
WindowsEvent
| where EventID == 1102 and Provider =~ "Microsoft-Windows-Eventlog"
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend Activity= "1102 - The audit log was cleared."
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), EventCount = count() by Computer, Account, EventID, Activity
Applied to the Combined Result
| extend Name=tostring(split(Account, "@")[0]), UPNSuffix=tostring(split(Account, "@")[1])
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"1102" |
EventSourceName | eq |
| field:"Category" kind:eq value:"Microsoft-Windows-Eventlog" |
Provider | eq |
| field:"Provider" kind:eq value:"Microsoft-Windows-Eventlog" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | summarize |
Activity | summarize |
Computer | summarize |
EndTimeUtc | summarize |
EventCount | summarize |
EventID | summarize |
StartTimeUtc | summarize |
Name | extend |
UPNSuffix | extend |
DnsDomain | extend |
HostName | extend |
Trust Monitor Event
#'This query identifies when a new trust monitor event is detected.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 8dcf7238-a7d0-4cfd-8d0c-b230e3cd9182
name: Trust Monitor Event
description: |
'This query identifies when a new trust monitor event is detected.'
severity: Medium
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1528
- T1555
query: |
let timeframe = ago(5m);
DuoSecurityTrustMonitor_CL
| where TimeGenerated >= timeframe
| extend AccountName = tostring(split(surfaced_auth_user_name_s, "@")[0]), AccountUPNSuffix = tostring(split(surfaced_auth_user_name_s, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: surfaced_auth_user_name_s
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: surfaced_auth_access_device_ip_s
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: SecurityJedi
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let timeframe = ago(5m);
Stage 1: source
DuoSecurityTrustMonitor_CL
Stage 2: where
| where TimeGenerated >= timeframe
Stage 3: extend
| extend AccountName = tostring(split(surfaced_auth_user_name_s, "@")[0]), AccountUPNSuffix = tostring(split(surfaced_auth_user_name_s, "@")[1])
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | extend |
AccountUPNSuffix | extend |
Ubiquiti - Connection to known malicious IP or C2
#'Detects allowed connections to IP addresses which are in TI list and are known to be malicious.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: db60ca0b-b668-439b-b889-b63b57ef20fb
name: Ubiquiti - Connection to known malicious IP or C2
description: |
'Detects allowed connections to IP addresses which are in TI list and are known to be malicious.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- CommandAndControl
relevantTechniques:
- T1071
- T1571
- T1572
query: |
let malicious_ips =
ThreatIntelligenceIndicator
| where isnotempty(NetworkIP)
| summarize make_list(NetworkIP);
UbiquitiAuditEvent
| where EventCategory =~ 'firewall'
| where ipv4_is_private(SrcIpAddr)
| where ipv4_is_private(DstIpAddr) == 'False'
| where DstIpAddr in (malicious_ips)
| where DvcAction =~ 'Accepted' or DvcAction =~ 'Other'
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Let Binding: malicious_ips
let malicious_ips = ThreatIntelligenceIndicator
| where isnotempty(NetworkIP)
| summarize make_list(NetworkIP);
Stage 1: source
UbiquitiAuditEvent
Stage 2: where
| where EventCategory =~ 'firewall'
Stage 3: where
| where ipv4_is_private(SrcIpAddr)
Stage 4: where
| where ipv4_is_private(DstIpAddr) == 'False'
Stage 5: where
| where DstIpAddr in (malicious_ips)
Stage 6: where
| where DvcAction =~ 'Accepted' or DvcAction =~ 'Other'
Stage 7: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstIpAddr | in |
| field:"dest_ip" kind:in value:"malicious_ips" |
DvcAction | eq |
| field:"DvcAction" kind:eq |
EventCategory | eq |
| field:"EventCategory" kind:eq value:"firewall" |
SrcIpAddr | cidr_match |
| field:"src_ip" kind:cidr_match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstIpAddr | 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:DstIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
VMware ESXi - Root password changed
#'Detects when root user password is changed.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Stealth |
Rule Body
id: 17b0ea43-5aeb-4dc4-ac3a-be84acb8d5b7
name: VMware ESXi - Root password changed
description: |
'Detects when root user password is changed.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
- DefenseEvasion
relevantTechniques:
- T1078
- T1098
- T1556
query: |
VMwareESXi
| where SyslogMessage has_any ("password changed for root","Password was changed for account root")
| extend SrcIpAddr = extract(@'root@(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1, SyslogMessage)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
VMwareESXi
Stage 2: where
| where SyslogMessage has_any ("password changed for root","Password was changed for account root")
Stage 3: extend
| extend SrcIpAddr = extract(@'root@(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 1, SyslogMessage)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | extend |
WAN Accelerator Deleted
#Detects when a WAN accelerator is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: 5b19f19b-d92c-486f-be98-ba2c5945e240
name: WAN Accelerator Deleted
description: Detects when a WAN accelerator is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 27300\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 27300
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"27300" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |