Detection rules › Kusto
Kusto rules: 1y
Abnormal Security - Account Takeover case opened
#'Creates an incident when Abnormal Security opens an Account Takeover (ATO) case, indicating a mailbox or user account is suspected to be compromised. The ATO case severity, status, and observed indicators are surfaced for triage.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Rule Body
id: da243bf4-382b-46b9-9b4d-ce6ffe9e7beb
name: Abnormal Security - Account Takeover case opened
description: |
'Creates an incident when Abnormal Security opens an Account Takeover (ATO) case,
indicating a mailbox or user account is suspected to be compromised. The ATO case
severity, status, and observed indicators are surfaced for triage.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: AbnormalSecurityPush
dataTypes:
- ABNORMAL_SECURITY_ATO_CASE_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
ABNORMAL_SECURITY_ATO_CASE_CL
| where isnotempty(abx_body_abx_body_ato_case_id_s)
// Collapse to one row per ATO case (earliest event in the window) so repeated
// events for the same case do not raise duplicate alerts.
| summarize arg_min(TimeGenerated, *) by CaseId = abx_body_abx_body_ato_case_id_s
| extend
CompromisedAccount = abx_body_abx_body_compromised_account_s,
CaseSeverity = abx_body_abx_body_severity_s,
CaseStatus = abx_body_abx_body_status_s,
Indicators = abx_body_abx_body_indicators_s,
FirstDetected = abx_body_abx_body_first_detected_t
| project TimeGenerated, CompromisedAccount, CaseId, CaseSeverity, CaseStatus, Indicators, FirstDetected
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CompromisedAccount
customDetails:
AtoCaseId: CaseId
CaseSeverity: CaseSeverity
CaseStatus: CaseStatus
alertDetailsOverride:
alertDisplayNameFormat: "Abnormal Security: Account Takeover case for {{CompromisedAccount}}"
alertDescriptionFormat: "Abnormal Security opened ATO case {{CaseId}} (severity {{CaseSeverity}}) for the compromised account {{CompromisedAccount}}."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
ABNORMAL_SECURITY_ATO_CASE_CL
Stage 2: where
| where isnotempty(abx_body_abx_body_ato_case_id_s)
Stage 3: summarize
| summarize arg_min(TimeGenerated, *) by CaseId = abx_body_abx_body_ato_case_id_s
Stage 4: extend
| extend
CompromisedAccount = abx_body_abx_body_compromised_account_s,
CaseSeverity = abx_body_abx_body_severity_s,
CaseStatus = abx_body_abx_body_status_s,
Indicators = abx_body_abx_body_indicators_s,
FirstDetected = abx_body_abx_body_first_detected_t
Stage 5: project
| project TimeGenerated, CompromisedAccount, CaseId, CaseSeverity, CaseStatus, Indicators, FirstDetected
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
abx_body_abx_body_ato_case_id_s | is_not_null | field:"abx_body_abx_body_ato_case_id_s" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CaseId | project |
CaseSeverity | project |
CaseStatus | project |
CompromisedAccount | project |
FirstDetected | project |
Indicators | project |
TimeGenerated | project |
Azure DevOps Personal Access Token (PAT) misuse
#'This Alert detects whenever a PAT is used in ways that PATs are not normally used. May require an allow list and baselining. Reference - https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page Use this query for baselining: ADOAuditLogs | distinct OperationName'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Impact |
Rule Body
id: ac891683-53c3-4f86-86b4-c361708e2b2b
name: Azure DevOps Personal Access Token (PAT) misuse
description: |
'This Alert detects whenever a PAT is used in ways that PATs are not normally used. May require an allow list and baselining.
Reference - https://docs.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page
Use this query for baselining:
ADOAuditLogs
| distinct OperationName'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- Impact
relevantTechniques:
- T1496
- T1559
query: |
// Allowlisted UPNs should likely stay empty
let AllowlistedUpns = datatable(UPN:string)['foo@bar.com', 'test@foo.com'];
// Operation Name parts that will alert
let HasAnyBlocklist = datatable(OperationNamePart:string)['Security.','Project.','AuditLog.','Extension.'];
// Distinct Operation Names that will flag
let HasExactBlocklist = datatable(OperationName:string)['Group.UpdateGroupMembership.Add','Library.ServiceConnectionExecuted','Pipelines.PipelineModified',
'Release.ReleasePipelineModified', 'Git.RefUpdatePoliciesBypassed'];
ADOAuditLogs
| where AuthenticationMechanism startswith "PAT" and (OperationName has_any (HasAnyBlocklist) or OperationName in (HasExactBlocklist))
and ActorUPN !in (AllowlistedUpns)
| project TimeGenerated, AuthenticationMechanism, ProjectName, ActorUPN, ActorDisplayName, IpAddress, UserAgent, OperationName, Details, Data
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.6
kind: Scheduled
Stages and Predicates
Let Binding: AllowlistedUpns
let AllowlistedUpns = datatable(UPN:string)['foo@bar.com', 'test@foo.com'];
Let Binding: HasAnyBlocklist
let HasAnyBlocklist = datatable(OperationNamePart:string)['Security.','Project.','AuditLog.','Extension.'];
Let Binding: HasExactBlocklist
let HasExactBlocklist = datatable(OperationName:string)['Group.UpdateGroupMembership.Add','Library.ServiceConnectionExecuted','Pipelines.PipelineModified',
'Release.ReleasePipelineModified', 'Git.RefUpdatePoliciesBypassed'];
Stage 1: source
ADOAuditLogs
Stage 2: where
| where AuthenticationMechanism startswith "PAT" and (OperationName has_any (HasAnyBlocklist) or OperationName in (HasExactBlocklist))
and ActorUPN !in (AllowlistedUpns)
Stage 3: project
| project TimeGenerated, AuthenticationMechanism, ProjectName, ActorUPN, ActorDisplayName, IpAddress, UserAgent, OperationName, Details, Data
Stage 4: extend
| extend timestamp = TimeGenerated
Stage 5: extend
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AuthenticationMechanism | starts_with |
| field:"AuthenticationMechanism" kind:starts_with value:"PAT" |
OperationName | in |
| field:"OperationName" kind:in value:"HasExactBlocklist" |
OperationName | match |
| field:"OperationName" kind:match value:"HasAnyBlocklist" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ActorUPN | eq | AllowlistedUpns | excludes:ActorUPN field:"ActorUPN" value:"AllowlistedUpns" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorDisplayName | project |
ActorUPN | project |
AuthenticationMechanism | project |
Data | project |
Details | project |
IpAddress | project |
OperationName | project |
ProjectName | project |
TimeGenerated | project |
UserAgent | project |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
BitSight - drop in the headline rating
#'Rule helps to detect if headline ratings is drop in BitSight.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Command & Control |
Rule Body
id: b11fdc35-6368-4cc0-8128-52cd2e2cdda0
name: BitSight - drop in the headline rating
description: |
'Rule helps to detect if headline ratings is drop in BitSight.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: BitSight
dataTypes:
- BitSightGraphData
queryFrequency: 1d
queryPeriod: 24h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
- Reconnaissance
- CommandAndControl
relevantTechniques:
- T1591
- T1090
query: |
let timeframe = 24h;
BitSightGraphData
| where ingestion_time() > ago(timeframe)
| where toint(RatingDifferance) < 0
| project RatingDate, Rating, CompanyName, RatingDifferance
incidentConfiguration:
createIncident: false
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'BitSight : Alert for drop in the headline rating of {{CompanyName}}.'
alertDescriptionFormat: 'Alert is generated for {{CompanyName}}.\n\nRating Date: {{RatingDate}}\nRating Drop: {{RatingDifferance}}'
customDetails:
CompanyName: CompanyName
CompanyRating: Rating
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 24h;
Stage 1: source
BitSightGraphData
Stage 2: where
| where ingestion_time() > ago(timeframe)
Stage 3: where
| where toint(RatingDifferance) < 0
Stage 4: project
| project RatingDate, Rating, CompanyName, RatingDifferance
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RatingDifferance | lt |
| field:"RatingDifferance" kind:lt value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CompanyName | project |
Rating | project |
RatingDate | project |
RatingDifferance | project |
BloodHound Attack Path Finding - Key Vault Contributor 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 - Key Vault Contributor Role on Tier Zero Resource
Rule Body
id: 7b7347d2-3efb-47d9-a329-5b7ae4357d70
name: BloodHound Attack Path Finding - Key Vault Contributor Role on Tier Zero Resource
description: BloodHound Attack Path Finding - Key Vault Contributor 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 == "AzureT0KeyVaultContributor"
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 == "AzureT0KeyVaultContributor"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0KeyVaultContributor" |
Commvault Cloud Alert
#'This query identifies Alerts from Commvault Cloud.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Rule Body
id: 317e757e-c320-448e-8837-fc61a70fe609
name: Commvault Cloud Alert
description: |
'This query identifies Alerts from Commvault Cloud.'
severity: Medium
status: Available
enabled: true
requiredDataConnectors:
- connectorId: CommvaultSecurityIQConnector
datatypes:
- CommvaultAlertsCCF_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Impact
customDetails:
Client: ClientName
AnomalyType: AnomalyType
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'Commvault Cloud Anomaly detected for client: {{ClientName}}'
alertDescriptionFormat: 'Anomaly detected for Commvault client {{ClientName}}. AnomalyType flag: {{AnomalyType}}.'
alertDynamicProperties: []
relevantTechniques:
- T1578
- T1531
tags:
- Commvault
- Metallic
- Threat Intelligence
- Ransomware
query: |
CommvaultAlertsCCF_CL
| where TimeGenerated > ago(5m)
| where isnotnull(AnomalyType) and AnomalyType > 0
| take 1000
entityMappings: null
version: 1.0.6
kind: Scheduled
Stages and Predicates
Stage 1: source
CommvaultAlertsCCF_CL
Stage 2: where
| where TimeGenerated > ago(5m)
Stage 3: where
| where isnotnull(AnomalyType) and AnomalyType > 0
Stage 4: take
| take 1000
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AnomalyType | gt |
| field:"AnomalyType" kind:gt value:"0" |
AnomalyType | is_not_null | field:"AnomalyType" kind:is_not_null |
Conditional Access Policy Modified by New User
#Detects a Conditional Access Policy being modified by a user who has not modified a policy in the last 14 days. A threat actor may try to modify policies to weaken the security controls in place. Investigate any change to ensure they are approved. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-infrastructure#conditional-access
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure MFA Disabled (Panther)
- Azure Policy Violation Detected (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
Rule Body
id: 25a7f951-54b7-4cf5-9862-ebc04306c590
name: Conditional Access Policy Modified by New User
description: |
'Detects a Conditional Access Policy being modified by a user who has not modified a policy in the last 14 days.
A threat actor may try to modify policies to weaken the security controls in place.
Investigate any change to ensure they are approved.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-infrastructure#conditional-access'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let known_users = (AuditLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where OperationName has "conditional access policy"
| where Result =~ "success"
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| summarize by InitiatingUserPrincipalName);
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName has "conditional access policy"
| where Result =~ "success"
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppId = tostring(InitiatedBy.app.appId)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CAPolicyName = tostring(TargetResources[0].displayName)
| where InitiatingUserPrincipalName !in (known_users)
| extend NewPolicyValues = TargetResources[0].modifiedProperties[0].newValue
| extend OldPolicyValues = TargetResources[0].modifiedProperties[0].oldValue
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| project-reorder TimeGenerated, OperationName, CAPolicyName, InitiatingAppId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, NewPolicyValues, OldPolicyValues
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIPAddress
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: InitiatingAppId
- identifier: Name
columnName: InitiatingAppName
version: 1.1.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Let Binding: known_users
let known_users = (AuditLogs
| where TimeGenerated between(ago(14d)..ago(1d))
| where OperationName has "conditional access policy"
| where Result =~ "success"
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| summarize by InitiatingUserPrincipalName);
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where OperationName has "conditional access policy"
Stage 4: where
| where Result =~ "success"
Stage 5: extend (7 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppId = tostring(InitiatedBy.app.appId)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CAPolicyName = tostring(TargetResources[0].displayName)
Stage 6: where
| where InitiatingUserPrincipalName !in (known_users)
Stage 7: extend (3 consecutive steps)
| extend NewPolicyValues = TargetResources[0].modifiedProperties[0].newValue
| extend OldPolicyValues = TargetResources[0].modifiedProperties[0].oldValue
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Stage 8: project-reorder
| project-reorder TimeGenerated, OperationName, CAPolicyName, InitiatingAppId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, NewPolicyValues, OldPolicyValues
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | match |
| field:"OperationName" kind:match value:"conditional access policy" |
Result | eq |
| field:"Result" kind:eq value:"success" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
InitiatingUserPrincipalName | eq | known_users | excludes:InitiatingUserPrincipalName field:"InitiatingUserPrincipalName" value:"known_users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppId | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIPAddress | extend |
CAPolicyName | extend |
NewPolicyValues | extend |
OldPolicyValues | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
Credential errors stateful anomaly on database
#'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When Brute Force attacks are attempted, majority of logins will use wrong credentials, thus will fail with error code 18456. Thus, if we see a large number of logins with such error codes, this could indicate Brute Force attack.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Rules Detecting the Same Action
These rules filter on the same operation.
- Affected rows stateful anomaly on database (Kusto)
- Drop attempts stateful anomaly on database (Kusto)
- Execution attempts stateful anomaly on database (Kusto)
- Firewall errors stateful anomaly on database (Kusto)
- Firewall rule manipulation attempts stateful anomaly on database (Kusto)
- OLE object manipulation attempts stateful anomaly on database (Kusto)
- Outgoing connection attempts stateful anomaly on database (Kusto)
- Response rows stateful anomaly on database (Kusto)
Rule Body
id: daa32afa-b5b6-427d-93e9-e32f3f359dd7
name: Credential errors stateful anomaly on database
description: |
'This query batches of distinct SQL queries that failed with error codes that might indicate malicious attempts to gain illegitimate access to the data. When Brute Force attacks are attempted, majority of logins will use wrong credentials, thus will fail with error code 18456. Thus, if we see a large number of logins with such error codes, this could indicate Brute Force attack.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureSql
dataTypes:
- AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1190
- T1110.001
- T1110.002
tags:
- SQL
query: |
let monitoredStatementsThreshold = 1; // Minimal number of monitored statements in the slice to trigger an anomaly.
let trainingSlicesThreshold = 5; // The maximal amount of slices with monitored statements in the training window before anomaly detection is throttled.
let timeSliceSize = 1h; // The size of the single timeSlice for individual aggregation.
let detectionWindow = 1h; // The size of the recent detection window for detecting anomalies.
let trainingWindow = detectionWindow + 14d; // The size of the training window before the detection window for learning the normal state.
let monitoredErrors = pack_array(18456); // List of sql error codes relevant for this detection.
let processedData = materialize (
AzureDiagnostics
| where TimeGenerated >= ago(trainingWindow)
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s,
Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
, 0),
State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
, 0),
AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
, anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
| where WindowType in ('detection', 'training'));
let trainingSet =
processedData
| where WindowType == 'training'
| summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
by Database;
processedData
| where WindowType == 'detection'
| join kind = inner (trainingSet) on Database
| extend IsErrorAnomalyOnStatement = iff(((countStatementsWithError >= monitoredStatementsThreshold) and (countSlicesWithErrors <= trainingSlicesThreshold)), true, false)
, anomalyScore = round(countStatementsWithError/monitoredStatementsThreshold, 0)
| where IsErrorAnomalyOnStatement == 'true'
| project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithError, anyMonitoredStatement, anyInfo, anomalyScore
| extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ApplicationName
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: ResourceId
alertDetailsOverride:
alertDisplayNameFormat: 'Credential errors stateful anomaly on database {{Database}}'
alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithError}} statements with monitored errors in the last hour. Investigate the database activity for potential credential brute force attacks.'
version: 1.1.2
kind: Scheduled
Stages and Predicates
Parameters
let monitoredStatementsThreshold = 1;
let trainingSlicesThreshold = 5;
let timeSliceSize = 1h;
let detectionWindow = 1h;
let trainingWindow = detectionWindow + 14d;
let monitoredErrors = pack_array(18456);
let processedData is inlined into the numbered stages below.
Let Binding: trainingSet
let trainingSet = processedData
| where WindowType == 'training'
| summarize countSlicesWithErrors = dcountif(timeSlice, countStatementsWithError >= monitoredStatementsThreshold)
by Database;
Stages 1 to 7 define let processedData as the rule's main pipeline source. Stages 8 to 13 use it.
Stage 1: source
AzureDiagnostics
Stage 2: where
| where TimeGenerated >= ago(trainingWindow)
Stage 3: where
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM")
Stage 4: project
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s,
Error = case( additional_information_s has 'error_code', toint(extract("<error_code>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+)", 1, additional_information_s))
, 0),
State = case( additional_information_s has 'error_state', toint(extract("<error_state>([0-9.]+)", 1, additional_information_s))
, additional_information_s has 'failure_reason', toint(extract("<failure_reason>Err ([0-9.]+), Level ([0-9.]+)", 2, additional_information_s))
, 0),
AdditionalInfo = additional_information_s, timeSlice = floor(TimeGenerated, timeSliceSize)
Stage 5: summarize
| summarize countEvents = count(), countStatements = dcount(Statement), countStatementsWithError = dcountif(Statement, Error in (monitoredErrors))
, anyMonitoredStatement = anyif(Statement, Error in (monitoredErrors)), anyInfo = anyif(AdditionalInfo, Error in (monitoredErrors))
by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
Stage 6: extend
| extend WindowType = case( timeSlice >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= timeSlice and timeSlice < ago(detectionWindow)), 'training', 'other')
WindowType =/* macro: (timeSlice >= ago(detectionWindow)) */'detection'/* macro: (ago(trainingWindow) <= timeSlice) */'training''other'Stage 7: where
| where WindowType in ('detection', 'training')
Stage 8: where
processedData
| where WindowType == 'detection'
Stage 9: join
| join kind = inner (trainingSet) on Database
Stage 10: extend
extend IsErrorAnomalyOnStatement, anomalyScore
IsErrorAnomalyOnStatement =countStatementsWithError >= monitoredStatementsThreshold and countSlicesWithErrors <= trainingSlicesThresholdtruefalseStage 11: where
where IsErrorAnomalyOnStatement == true
Stage 12: project
project ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, TimeGenerated, anomalyScore, anyInfo, anyMonitoredStatement, countEvents, countStatements, countStatementsWithError
Stage 13: extend
extend Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"SQLSecurityAuditEvents" |
IsErrorAnomalyOnStatement | eq |
| field:"IsErrorAnomalyOnStatement" kind:eq value:"true" |
WindowType | eq |
| field:"WindowType" kind:eq |
WindowType | in |
| field:"WindowType" kind:in |
action_id_s | match |
| field:"action_id_s" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ApplicationName | project |
ClientIp | project |
Database | project |
HostName | project |
PrincipalName | project |
ResourceId | project |
TimeGenerated | project |
anomalyScore | project |
anyInfo | project |
anyMonitoredStatement | project |
countEvents | project |
countStatements | project |
countStatementsWithError | project |
Name | extend |
UPNSuffix | extend |
Critical Threat Detected
#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 creates an incident in the event a critical threat was identified on a Carbon Black managed endpoint.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Rule Body
id: 2ca4e7fc-c61a-49e5-9736-5da8035c47e0
name: Critical Threat Detected
description: |
'This creates an incident in the event a critical threat was identified on a Carbon Black managed endpoint.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VMwareCarbonBlack
dataTypes:
- CarbonBlackNotifications_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1210
query: |
let threshold = 8;
CarbonBlackNotifications_CL
| where threatHunterInfo_score_d >= threshold
| extend eventTime = datetime(1970-01-01) + tolong(threatHunterInfo_time_d/1000) * 1sec
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by eventTime, Threat_Name = threatHunterInfo_reportName_s, Device_Name = deviceInfo_deviceName_s, Internal_IP = deviceInfo_internalIpAddress_s, External_IP = deviceInfo_externalIpAddress_s, Threat_Score = threatHunterInfo_score_d
| project-away count_
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Device_Name
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Internal_IP
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 8;
Stage 1: source
CarbonBlackNotifications_CL
Stage 2: where
| where threatHunterInfo_score_d >= threshold
Stage 3: extend
| extend eventTime = datetime(1970-01-01) + tolong(threatHunterInfo_time_d/1000) * 1sec
Stage 4: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by eventTime, Threat_Name = threatHunterInfo_reportName_s, Device_Name = deviceInfo_deviceName_s, Internal_IP = deviceInfo_internalIpAddress_s, External_IP = deviceInfo_externalIpAddress_s, Threat_Score = threatHunterInfo_score_d
Stage 5: project-away
| project-away count_
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
threatHunterInfo_score_d | ge |
| field:"threatHunterInfo_score_d" kind:ge value:"8" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Device_Name | summarize |
EndTime | summarize |
External_IP | summarize |
Internal_IP | summarize |
StartTime | summarize |
Threat_Name | summarize |
Threat_Score | summarize |
eventTime | summarize |
CYFIRMA - Brand Intelligence - Domain Impersonation 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 analytics rule detects high-risk domain impersonation activity, where newly registered or existing domains closely resemble the legitimate brand name or organizational assets. These suspicious domains may use typosquatting, homoglyphs, or brand keywords to mislead users, steal credentials, or host phishing/malicious content. The domains are identified through CYFIRMA's external threat intelligence feeds and flagged due to potential misuse in impersonation, fraud, or social engineering attacks targeting employees, customers, or partners. Early detection of these domains enables proactive mitigation measures such as domain takedown, DNS blocking, and awareness campaigns."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Initial Access | |
| Command & Control |
Rule Body
id: 10bdf525-5b89-4a25-933a-e63e73b915f1
name: CYFIRMA - Brand Intelligence - Domain Impersonation High Rule
description: |
"This analytics rule detects high-risk domain impersonation activity, where newly registered or existing domains closely resemble the legitimate brand name or organizational assets.
These suspicious domains may use typosquatting, homoglyphs, or brand keywords to mislead users, steal credentials, or host phishing/malicious content.
The domains are identified through CYFIRMA's external threat intelligence feeds and flagged due to potential misuse in impersonation, fraud, or social engineering attacks targeting employees, customers, or partners.
Early detection of these domains enables proactive mitigation measures such as domain takedown, DNS blocking, and awareness campaigns."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaBrandIntelligenceAlertsDC
dataTypes:
- CyfirmaBIDomainITAssetAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- ResourceDevelopment
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1583.001
- T1586.002
- T1566.002
- T1566.001
- T1071.003
- T1071.001
query: |
// High severity- Brand Intelligence - Domain Impersonation
let timeFrame = 5m;
CyfirmaBIDomainITAssetAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
Domain=domain,
DRDomain=dr_domain,
DRSubDomain=dr_sub_domain,
DomainSquat=signature,
HostProvider=host_provider,
RegisteredDate=registered_date,
CreatedDate=created_date,
ThreatActor=suspected_threat_actor,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
Domain,
DRDomain,
DRSubDomain,
DomainSquat,
HostProvider,
RegisteredDate,
CreatedDate,
ThreatActor,
ProductName,
ProviderName
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DomainSquat
customDetails:
TimeGenerated: TimeGenerated
RiskScore: RiskScore
DomainSquat: DomainSquat
Domain: Domain
Description: Description
DRDomain: DRDomain
DRSubDomain: DRSubDomain
AlertUID: AlertUID
UID: UID
ThreatActor: ThreatActor
FirstSeen: FirstSeen
LastSeen: LastSeen
CreatedDate: CreatedDate
RegisteredDate: RegisteredDate
HostProvider: HostProvider
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Alert - Malicious Domain Impersonation of Corporate Brand - Domain Squat: {{DomainSquat}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaBIDomainITAssetAlerts_CL
Stage 2: where
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
Domain=domain,
DRDomain=dr_domain,
DRSubDomain=dr_sub_domain,
DomainSquat=signature,
HostProvider=host_provider,
RegisteredDate=registered_date,
CreatedDate=created_date,
ThreatActor=suspected_threat_actor,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
Domain,
DRDomain,
DRSubDomain,
DomainSquat,
HostProvider,
RegisteredDate,
CreatedDate,
ThreatActor,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
CreatedDate | project |
DRDomain | project |
DRSubDomain | project |
Description | project |
Domain | project |
DomainSquat | project |
FirstSeen | project |
HostProvider | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
RegisteredDate | project |
RiskScore | project |
ThreatActor | project |
TimeGenerated | project |
UID | project |
CYFIRMA - Data Breach and Web Monitoring - Phishing Campaign Detection 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.
"Detects phishing campaigns targeting enterprise domains, as identified through CYFIRMA's Data Breach and Dark Web Monitoring. These alerts may include malicious URLs used for credential harvesting, domain impersonation, or social engineering. Immediate triage and takedown actions are recommended."
MITRE ATT&CK Coverage
Rule Body
id: 17cce4fc-9b4c-4eef-a4c7-083b44545e6e
name: CYFIRMA - Data Breach and Web Monitoring - Phishing Campaign Detection Rule
description: |
"Detects phishing campaigns targeting enterprise domains, as identified through CYFIRMA's Data Breach and Dark Web Monitoring.
These alerts may include malicious URLs used for credential harvesting, domain impersonation, or social engineering.
Immediate triage and takedown actions are recommended."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaDigitalRiskAlertsConnector
dataTypes:
- CyfirmaDBWMPhishingAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Exfiltration
relevantTechniques:
- T1566.001
- T1566.002
- T1566.003
query: |
// High severity - Data Breach and Web Monitoring - Phishing Campaign Detection
let timeFrame = 5m;
CyfirmaDBWMPhishingAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Source=source,
Impact='',
Recommendation='',
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Source,
Impact,
Recommendation,
ProductName,
ProviderName,
AlertTitle
customDetails:
AssetType: AssetType
AssetValue: AssetValue
RiskScore: RiskScore
Source: Source
Impact: Impact
Description: Description
Recommendation: Recommendation
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
TimeGenerated: TimeGenerated
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Alert: Phishing Campaign Detection - {{AlertTitle}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaDBWMPhishingAlerts_CL
Stage 2: where
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Source=source,
Impact='',
Recommendation='',
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Source,
Impact,
Recommendation,
ProductName,
ProviderName,
AlertTitle
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 |
|---|---|
AlertTitle | project |
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
Recommendation | project |
RiskScore | project |
Source | project |
TimeGenerated | project |
UID | project |
Filewall - Blocked emails
#Identifies emails blocked by Filewall for Microsoft 365 (Exchange).
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 9b784b65-2d16-4c9f-9f59-2a5d4c659f42
name: Filewall - Blocked emails
description: Identifies emails blocked by Filewall for Microsoft 365 (Exchange).
severity: High
status: Available
kind: Scheduled
query: |
FilewallM365ExchangeEvent()
| where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'
| where EventType == 'ThreatInfo' and EventResult == 'Blocked'
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
relevantTechniques:
- T1048
tactics:
- Exfiltration
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
matchingMethod: AllEntities
reopenClosedIncident: false
enabled: false
lookbackDuration: PT1H
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: MailMessage
fieldMappings:
- identifier: Recipient
columnName: EmailRecipientTo
- identifier: Subject
columnName: EmailSubject
- identifier: Sender
columnName: EmailSenderFrom
requiredDataConnectors:
- dataTypes:
- FilewallExchange_CL
connectorId: FilewallM365
version: 1.0.0
Stages and Predicates
Stage 1: source
FilewallM365ExchangeEvent()
Stage 2: where
| where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'
Stage 3: where
| where EventType == 'ThreatInfo' and EventResult == 'Blocked'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventProduct | eq |
| field:"EventProduct" kind:eq value:"Filewall for Microsoft 365" |
EventResult | eq |
| field:"EventResult" kind:eq value:"Blocked" |
EventType | eq |
| field:"EventType" kind:eq value:"ThreatInfo" |
EventVendor | eq |
| field:"EventVendor" kind:eq value:"ODI-X" |
GitLab - TI - Connection from Malicious IP
#'This query correlates Threat Intelligence data from Microsoft Sentinel with GitLab NGINX Access Logs (available in GitLab CE as well) to identify access from potentially TI-flagged IPs.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 7241740a-5280-4b74-820a-862312d721a8
name: GitLab - TI - Connection from Malicious IP
description: |
'This query correlates Threat Intelligence data from Microsoft Sentinel with GitLab NGINX Access Logs (available in GitLab CE as well) to identify access from potentially TI-flagged IPs.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
ThreatIntelligenceIndicator
| where Action == true
// Picking up only IOCs that contain the entities we want
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
// Taking the first non-empty value based on potential IOC match availability
| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
//Exclude local addresses using ipv4_is_private operator
|where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
| join (GitLabAccess) on $left.TI_ipEntity == $right.IPAddress
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| project LatestIndicatorTime, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, TimeGenerated = EventTime, TI_ipEntity, IPAddress, URI
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPaddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where Action == true
Stage 3: where
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
Stage 4: extend (3 consecutive steps)
| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =isnotempty(NetworkIP)NetworkIPNetworkDestinationIPStage 5: where
| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
Stage 6: join
| join (GitLabAccess) on $left.TI_ipEntity == $right.IPAddress
Stage 7: summarize
summarize LatestIndicatorTime by IndicatorId
Stage 8: project
project ActivityGroupNames, ConfidenceScore, Description, ExpirationDateTime, IPAddress, IndicatorId, LatestIndicatorTime, TI_ipEntity, ThreatType, TimeGenerated, URI, Url
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"true" |
EmailSourceIpAddress | is_not_null | field:"EmailSourceIpAddress" kind:is_not_null | |
NetworkDestinationIP | is_not_null | field:"NetworkDestinationIP" kind:is_not_null | |
NetworkIP | is_not_null | field:"NetworkIP" kind:is_not_null | |
NetworkSourceIP | is_not_null | field:"NetworkSourceIP" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ConfidenceScore | project |
Description | project |
ExpirationDateTime | project |
IPAddress | project |
IndicatorId | project |
LatestIndicatorTime | project |
TI_ipEntity | project |
ThreatType | project |
TimeGenerated | project |
URI | project |
Url | project |
Java Executing cmd to run Powershell
#This query was originally published in the threat analytics report, Sysrv botnet evolution. Sysrv is a Go-based botnet that targets both Windows and Linux servers, and steals resources to mine cryptocurrency. The following query finds instances of the Java process being used to execute cmd.exe, and download and execute a PowerShell script.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 2c81c0a0-9823-4a14-b21a-2b4acd3335d2
name: Java Executing cmd to run Powershell
description: |
This query was originally published in the threat analytics report, Sysrv botnet evolution.
Sysrv is a Go-based botnet that targets both Windows and Linux servers, and steals resources to mine cryptocurrency.
The following query finds instances of the Java process being used to execute cmd.exe, and download and execute a PowerShell script.
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
relevantTechniques:
- T1059
tags:
- Sysrv
- Botnet
query: |
DeviceProcessEvents
| where InitiatingProcessFileName == 'java.exe' and FileName == 'cmd.exe'
and ProcessCommandLine has_all('powershell iex','DownloadString')
| 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
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where InitiatingProcessFileName == 'java.exe' and FileName == 'cmd.exe'
and ProcessCommandLine has_all('powershell iex','DownloadString')
Stage 3: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 4: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | eq |
| field:"process_basename" kind:eq value:"cmd.exe" |
InitiatingProcessFileName | eq |
| field:"parent_process_basename" kind:eq value:"java.exe" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostName | extend |
DnsDomain | extend |
Malware Event Detected
#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 restore points are marked as infected. This might indicate potential compromise of backup data.
Rule Body
id: b42424a6-10f4-447b-92a0-55ac38f4a475
name: Malware Event Detected
description: Detects when restore points are marked as infected. This might indicate
potential compromise of backup data.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
dataTypes:
- VeeamMalwareEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: VeeamMalwareEvents_CL
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: VbrHostName
version: 1.0.1
kind: Scheduled
customDetails:
VbrHostName: VbrHostName
MachineDisplayName: MachineDisplayName
MachineUuid: MachineUuid
BackupObjectId: MachineBackupObjectId
Stages and Predicates
Stage 1: source
VeeamMalwareEvents_CL
Multi-Factor Authentication Token Revoked
#Detects when a multi-factor authentication token is revoked.
Rule Body
id: 4d8f5244-cb56-4e95-ba65-0a7bec114a13
name: Multi-Factor Authentication Token Revoked
description: Detects when a multi-factor authentication token is revoked.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40202\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 40202
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = 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:"40202" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
NGINX - Multiple user agents for single source
#'Detects requests with different user agents from one source in short timeframe.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 83a0b48f-1cb7-4b4f-a018-23c3203a239b
name: NGINX - Multiple user agents for single source
description: |
'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- NGINX_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 5;
NGINXHTTPServer
| summarize makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
| extend ua_count = array_length(set_HttpUserAgentOriginal)
| where ua_count > threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 5;
Stage 1: source
NGINXHTTPServer
Stage 2: summarize
| summarize makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 3: extend
| extend ua_count = array_length(set_HttpUserAgentOriginal)
Stage 4: where
| where ua_count > threshold
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ua_count | gt |
| field:"ua_count" kind:gt value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
ua_count | extend |
IPCustomEntity | extend |
Rename System Utilities
#Attackers often use LOLBINs that are renamed to avoid detection rules that are based on filenames. This rule detects renamed LOLBINs by first searching for all the known SHA1 hashes of the LOLBINs in your DeviceProcessEvents. This list is then used as reference to find other files executed which have a name that doesn't match the original filename. This query is really heavy on resources. Use it with care.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: 335ddff8-b615-42cd-b593-86e419b45d78
name: Rename System Utilities
description: |
Attackers often use LOLBINs that are renamed to avoid detection rules that are based on filenames.
This rule detects renamed LOLBINs by first searching for all the known SHA1 hashes of the LOLBINs in your DeviceProcessEvents. This list is then used as reference to find other files executed which have a name that doesn't match the original filename.
This query is really heavy on resources. Use it with care.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1036.003
query: |
// Removed update.exe as LOLBIN. It's a way too generic name and causes a lot of noise.
let lolbin = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]);
let allHashesLolbin = materialize(
DeviceProcessEvents
| where FileName in~ (lolbin) and not(isempty(SHA1))
| extend OriginalFilename = tolower(FileName)
| summarize by SHA1, OriginalFilename);
allHashesLolbin
| join kind=inner hint.strategy = broadcast DeviceProcessEvents on SHA1
| where FileName !~ OriginalFilename
| where not(OriginalFilename =~ "bash.exe" and FileName =~ "sh.exe")
| extend message=strcat("Usage of ", OriginalFilename, ". Renamed as ", FileName)
| project-reorder message
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: Account
fieldMappings:
- identifier: Sid
columnName: AccountSid
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled
Stages and Predicates
let allHashesLolbin is inlined into the numbered stages below.
Let Binding: lolbin
let lolbin = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]);
Stage 1: source
let allHashesLolbin
Stage 2: source
DeviceProcessEvents
Stage 3: where
where not (isempty(SHA1)) and FileName in~ ("AgentExecutor.exe", "Appvlp.exe", "At.exe", "Atbroker.exe", "Bash.exe", "Bginfo.exe", "Bitsadmin.exe", "Cdb.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Devtoolslauncher.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Dotnet.exe", "Dxcap.exe", "Esentutl.exe", "Eventvwr.exe", "Excel.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Mftrace.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdeploy.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Powerpnt.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "SQLToolsPS.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "Sqldumper.exe", "Sqlps.exe", "Squirrel.exe", "SyncAppvPublishingServer.exe", "Tracker.exe", "Ttdinject.exe", "Tttracer.exe", "Verclsid.exe", "Wab.exe", "Winword.exe", "Wmic.exe", "Wscript.exe", "Wsl.exe", "Wsreset.exe", "Xwizard.exe", "csi.exe", "dnx.exe", "msxsl.exe", "ntdsutil.exe", "rcsi.exe", "te.exe", "vbc.exe", "vsjitdebugger.exe")
Stage 4: extend
extend OriginalFilename
Stage 5: summarize
summarize by SHA1, OriginalFilename
Stage 6: join
join kind=inner (DeviceProcessEvents) on SHA1
Stage 7: where
where FileName != OriginalFilename
Stage 8: where
where not ((FileName =~ "sh.exe" and OriginalFilename =~ "bash.exe"))
Stage 9: extend
extend message
Stage 10: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FileName | in |
| field:"FileName" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SHA1 | is_null | excludes:SHA1 | |
FileName | eq | sh.exe | excludes:FileName field:"FileName" value:"sh.exe" |
OriginalFilename | eq | bash.exe | excludes:OriginalFilename field:"OriginalFilename" value:"bash.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
OriginalFilename | summarize |
SHA1 | summarize |
message | extend |
Rubrik Threat Monitoring
#'Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Rule Body
id: 0083cbc4-776e-42ca-8694-6950fd605df9
name: Rubrik Threat Monitoring
version: 1.0.0
kind: Scheduled
description: |
'Rubrik Threat Monitoring matches Event Name and if match found then generate the incident for each object.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: RubrikSecurityCloudAzureFunctions
dataTypes:
- RubrikEventsData
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1546
query: |
Rubrik_Events_Data_CL
| where custom_details_eventName_s endswith "AnalysisMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringHashCatalogAnalysisFailed" or custom_details_eventName_s contains "ThreatMonitoringHashMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringYaraMatchesFound"
| extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, summary_s)), yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, summary_s)), fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, summary_s))
| extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28)
| where count_ > 0
| summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P7D
matchingMethod: Selected
groupByCustomDetails:
- ObjectName
- ObjectId
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: ThreatMonitoring Found {{count_}} {{eventname}} Matches for {{custom_details_objectName_s}}
customDetails:
EventName: custom_details_eventName_s
Summary: summary_s
ClusterIdentifier: custom_details_clusterId_g
ObjectId: custom_details_objectId_g
ObjectName: custom_details_objectName_s
ObjectType: custom_details_objectType_s
Url: custom_details_url_s
Stages and Predicates
Stage 1: source
Rubrik_Events_Data_CL
Stage 2: where
| where custom_details_eventName_s endswith "AnalysisMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringHashCatalogAnalysisFailed" or custom_details_eventName_s contains "ThreatMonitoringHashMatchesFound" or custom_details_eventName_s contains "ThreatMonitoringYaraMatchesFound"
Stage 3: extend
| extend hashMatchCount = toint(extract("Found ([0-9]+) hash matches",1, summary_s)), yaraMatchCount = toint(extract("Found ([0-9]+) YARA rule matches", 1, summary_s)), fileHashMatchCount = toint(extract("Found file hash matches for ([0-9]+) files", 1, summary_s))
Stage 4: extend
| extend count_ = coalesce(hashMatchCount,yaraMatchCount, fileHashMatchCount), eventname = substring(custom_details_eventName_s,16, strlen(custom_details_eventName_s) - 28)
Stage 5: where
| where count_ > 0
Stage 6: summarize
| summarize arg_max(TimeGenerated,*) by eventname, custom_details_objectId_g, count_
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
count_ | gt |
| field:"count_" kind:gt value:"0" |
custom_details_eventName_s | contains |
| field:"custom_details_eventName_s" kind:contains |
custom_details_eventName_s | ends_with |
| field:"custom_details_eventName_s" kind:ends_with value:"AnalysisMatchesFound" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
count_ | summarize |
custom_details_objectId_g | summarize |
eventname | summarize |
Semperis DSP Zerologon vulnerability
#'This indicator looks for security vulnerability to CVE-2020-1472, which was patched by Microsoft in August 2020. Without this patch, an unauthenticated attacker can exploit CVE-2020-1472 to elevate their privileges and get administrative access on the domain.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rule Body
id: 85c1f9e4-6f14-46bf-82d5-dbe495b92aab
name: Semperis DSP Zerologon vulnerability
description: |
'This indicator looks for security vulnerability to CVE-2020-1472, which was patched by Microsoft in August 2020. Without this patch, an unauthenticated attacker can exploit CVE-2020-1472 to elevate their privileges and get administrative access on the domain.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SemperisDSP
dataTypes:
- dsp_parser
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1068
query: |
dsp_parser
| where EventID == 9212
| where SecurityIndicatorName == "Zerologon vulnerability"
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 2.0.7
kind: Scheduled
Stages and Predicates
Stage 1: source
dsp_parser
Stage 2: where
| where EventID == 9212
Stage 3: where
| where SecurityIndicatorName == "Zerologon vulnerability"
Stage 4: extend
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"9212" |
SecurityIndicatorName | eq |
| field:"SecurityIndicatorName" kind:eq value:"Zerologon vulnerability" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
Snowflake - User granted admin privileges
#'Detects when user asigned admin privileges.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rule Body
id: 5ed33eee-0ab6-4bf5-9e9b-6100db83d39a
name: Snowflake - User granted admin privileges
description: |
'Detects when user asigned admin privileges.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Snowflake
dataTypes:
- Snowflake
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |
let adm_roles = dynamic(['SECURITYADMIN', 'SYSADMIN', 'ACCOUNTADMIN']);
Snowflake
| where QueryType =~ 'GRANT'
| where QueryText startswith 'GRANT ROLE'
| extend granted_role = extract(@'GRANT ROLE\s(\S+)\sto\suser', 1, QueryText)
| where granted_role in~ (adm_roles)
| extend usr = extract(@'GRANT ROLE\s(\S+)\sto\suser(\S+)', 2, QueryText)
| extend AccountCustomEntity = usr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let adm_roles = dynamic(['SECURITYADMIN', 'SYSADMIN', 'ACCOUNTADMIN']);
Stage 1: source
Snowflake
Stage 2: where
| where QueryType =~ 'GRANT'
Stage 3: where
| where QueryText startswith 'GRANT ROLE'
Stage 4: extend
| extend granted_role = extract(@'GRANT ROLE\s(\S+)\sto\suser', 1, QueryText)
Stage 5: where
| where granted_role in~ (adm_roles)
Stage 6: extend
| extend usr = extract(@'GRANT ROLE\s(\S+)\sto\suser(\S+)', 2, QueryText)
Stage 7: extend
| extend AccountCustomEntity = usr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
QueryText | starts_with |
| field:"QueryText" kind:starts_with value:"GRANT ROLE" |
QueryType | eq |
| field:"QueryType" kind:eq value:"GRANT" |
granted_role | in |
| field:"granted_role" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
granted_role | extend |
usr | extend |
AccountCustomEntity | extend |
StealthTalk - Login outside work zone
#Identifies a single StealthTalk authentication originating from a country or city that does not match the user's assigned (expected) geographic zone. Each individual mismatch is treated as an incident - there is no aggregation threshold, since a single login from an unexpected country is high-confidence evidence of a credential issue. An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs from AssignedCity. Source IPv4, raw event ID, and both the observed and expected geo-locations are surfaced as entities and custom details for the SOC analyst.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Stealth |
Rule Body
id: a7c3e9b1-4f5d-4e2a-9b8c-1d2e3f4a5b6c
name: StealthTalk - Login outside work zone
description: |
Identifies a single StealthTalk authentication originating from a country or city that does
not match the user's assigned (expected) geographic zone. Each individual mismatch is
treated as an incident - there is no aggregation threshold, since a single login from an
unexpected country is high-confidence evidence of a credential issue.
An incident fires when LoginCountry differs from AssignedCountry OR LoginCity differs
from AssignedCity. Source IPv4, raw event ID, and both the observed and expected
geo-locations are surfaced as entities and custom details for the SOC analyst.
severity: High
requiredDataConnectors:
- connectorId: StealthTalkAnomalousAuth
dataTypes:
- StealthTalkAnomalousAuth_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- DefenseEvasion
- CredentialAccess
relevantTechniques:
- T1078
query: |
let LookbackPeriod = 1h;
StealthTalkAnomalousAuth_CL
| where TimeGenerated >= ago(LookbackPeriod)
| where EventType == "GeoAnomalyLogin"
| where LoginCountry != AssignedCountry
or LoginCity != AssignedCity
| extend
CountryMismatch = LoginCountry != AssignedCountry,
CityMismatch = LoginCity != AssignedCity,
AlertName = "LoginOutsideWorkZone",
AlertDetails = strcat(
"User ", UserId,
" logged in from ", LoginCity, " (", LoginCountry, ")",
" - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
" Source IP: ", IpAddress, ".",
" Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
" City mismatch: ", tostring(LoginCity != AssignedCity), "."
)
| project
TimeGenerated, UserId, DeviceId,
LoginCountry, LoginCity, AssignedCountry, AssignedCity,
CountryMismatch, CityMismatch, IpAddress,
AppVersion, RawEventId, AlertName, AlertDetails
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserId
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
customDetails:
LoginCountry: LoginCountry
LoginCity: LoginCity
AssignedCountry: AssignedCountry
AssignedCity: AssignedCity
CountryMismatch: CountryMismatch
CityMismatch: CityMismatch
AppVersion: AppVersion
EventReference: RawEventId
alertDetailsOverride:
alertDisplayNameFormat: 'StealthTalk: Login Outside Work Zone - {{UserId}} from {{LoginCity}} ({{LoginCountry}})'
alertDescriptionFormat: '{{AlertDetails}}'
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: Selected
groupByEntities:
- Account
suppressionEnabled: false
suppressionDuration: 1h
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let LookbackPeriod = 1h;
Stage 1: source
StealthTalkAnomalousAuth_CL
Stage 2: where
| where TimeGenerated >= ago(LookbackPeriod)
Stage 3: where
| where EventType == "GeoAnomalyLogin"
Stage 4: where
| where LoginCountry != AssignedCountry
or LoginCity != AssignedCity
Stage 5: extend
| extend
CountryMismatch = LoginCountry != AssignedCountry,
CityMismatch = LoginCity != AssignedCity,
AlertName = "LoginOutsideWorkZone",
AlertDetails = strcat(
"User ", UserId,
" logged in from ", LoginCity, " (", LoginCountry, ")",
" - assigned zone: ", AssignedCity, " (", AssignedCountry, ").",
" Source IP: ", IpAddress, ".",
" Country mismatch: ", tostring(LoginCountry != AssignedCountry), ".",
" City mismatch: ", tostring(LoginCity != AssignedCity), "."
)
Stage 6: project
| project
TimeGenerated, UserId, DeviceId,
LoginCountry, LoginCity, AssignedCountry, AssignedCity,
CountryMismatch, CityMismatch, IpAddress,
AppVersion, RawEventId, AlertName, AlertDetails
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"GeoAnomalyLogin" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertDetails | project |
AlertName | project |
AppVersion | project |
AssignedCity | project |
AssignedCountry | project |
CityMismatch | project |
CountryMismatch | project |
DeviceId | project |
IpAddress | project |
LoginCity | project |
LoginCountry | project |
RawEventId | project |
TimeGenerated | project |
UserId | project |
Time series anomaly for data size transferred to public internet
#'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern. A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour. The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed . You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: f2dd4a3a-ebac-4994-9499-1a859938c947
name: Time series anomaly for data size transferred to public internet
description: |
'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern.
A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated.
The higher the score, the further it is from the baseline value.
The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour.
The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed .
You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
severity: Medium
requiredDataConnectors:
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: AzureMonitor(VMInsights)
dataTypes:
- VMConnection
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 1
tactics:
- Exfiltration
relevantTechniques:
- T1030
tags:
- DEV-0537
query: |
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let bytessentperhourthreshold = 10;
let TimeSeriesData = (union isfuzzy=true
(
VMConnection
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false
| extend DeviceVendor = "VMConnection"
| project TimeGenerated, BytesSent, DeviceVendor
| make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
),
(
CommonSecurityLog
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where ipv4_is_private(DestinationIP) == false
| project TimeGenerated, SentBytes, DeviceVendor
| make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
)
);
//Filter anomolies against TimeSeriesData
let TimeSeriesAlerts = materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(TotalBytesSent, scorethreshold, -1, 'linefit')
| mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend AnomalyHour = TimeGenerated
| extend TotalBytesSentinMBperHour = round(((TotalBytesSent / 1024)/1024),2), baselinebytessentperHour = round(((baseline / 1024)/1024),2), score = round(score,2)
| project DeviceVendor, AnomalyHour, TimeGenerated, TotalBytesSentinMBperHour, baselinebytessentperHour, anomalies, score);
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
//Union of all BaseLogs aggregated per hour
let BaseLogs = (union isfuzzy=true
(
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| where ipv4_is_private(DestinationIP) == false
| extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
| summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
| where Rank < 10 // Selecting Top 10 records with Highest BytesSent in each Hour
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
),
(
VMConnection
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false | extend DeviceVendor = "VMConnection"
| extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
| summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
| where Rank < 10 // Selecting Top 10 records with Highest BytesSent in each Hour
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
)
);
// Join against base logs to retrive records associated with the hour of anomoly
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join (
BaseLogs | extend AnomalyHour = TimeGeneratedHour
) on DeviceVendor, AnomalyHour | sort by score desc
| project DeviceVendor, AnomalyHour,TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
| summarize EventCount = count(), StartTimeUtc= min(TimeGeneratedMax), EndTimeUtc= max(TimeGeneratedMax), SourceIPMax= arg_max(SourceIP,*), TotalBytesSentinMB = sum(TotalSentBytesinMB), TotalBytesReceivedinMB = sum(TotalReceivedBytesinMB), SourceIPList = make_set(SourceIP, 100), DestinationIPList = make_set(DestinationIPList, 100) by AnomalyHour,TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
| project DeviceVendor, AnomalyHour, StartTimeUtc, EndTimeUtc, SourceIPMax, SourceIPList, DestinationIPList, DestinationPortList, TotalBytesSentinMB, TotalBytesReceivedinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies, EventCount
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIPMax
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
Parameters
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let bytessentperhourthreshold = 10;
let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.
Let Binding: AnomalyHours
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
Let Binding: BaseLogs
let BaseLogs = (union isfuzzy=true
(
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| where ipv4_is_private(DestinationIP) == false
| extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
| summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
),
(
VMConnection
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| where ipv4_is_private(DestinationIP) == false | extend DeviceVendor = "VMConnection"
| extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
| summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
)
);
Stages 1 to 21 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 22 to 27 use it.
Stage 1: union
union of 2 branches
Stage 2: source
VMConnection
Stage 3: where
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
Stage 4: where
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
Stage 5: extend
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
Stage 6: where
| where ipv4_is_private(DestinationIP) == false
Stage 7: extend
| extend DeviceVendor = "VMConnection"
Stage 8: project
| project TimeGenerated, BytesSent, DeviceVendor
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 9: make-series
| make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
Stage 10: source
CommonSecurityLog
Stage 11: where
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
Stage 12: where
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
Stage 13: where
| where ipv4_is_private(DestinationIP) == false
Stage 14: project
| project TimeGenerated, SentBytes, DeviceVendor
Stage 15: make-series
| make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
Stage 16: extend
extend anomalies, baseline, score
Stage 17: mv-expand
mv-expand TotalBytesSent
Stage 18: where
where anomalies > 0
Stage 19: extend
extend AnomalyHour
Stage 20: extend
extend TotalBytesSentinMBperHour, baselinebytessentperHour, score
Stage 21: project
project AnomalyHour, DeviceVendor, TimeGenerated, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score
Stage 22: where
where TimeGenerated > ago(172800s)
Stage 23: join
join (BaseLogs) on DeviceVendor, AnomalyHour
Stage 24: sort
sort by score
Stage 25: project
project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, SourceIP, TimeGeneratedMax, TotalBytesSentinMBperHour, TotalReceivedBytesinMB, TotalSentBytesinMB, anomalies, baselinebytessentperHour, score
Stage 26: summarize
summarize DestinationIPList, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB by AnomalyHour, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
Stage 27: project
project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DestinationIP | 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:DestinationIP |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AnomalyHour | project |
DestinationIPList | project |
DestinationPortList | project |
DeviceVendor | project |
EndTimeUtc | project |
EventCount | project |
SourceIPList | project |
SourceIPMax | project |
StartTimeUtc | project |
TotalBytesReceivedinMB | project |
TotalBytesSentinMB | project |
TotalBytesSentinMBperHour | project |
anomalies | project |
baselinebytessentperHour | project |
score | project |
Tomcat - Request from localhost IP address
#'Detects request from localhost IP address.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: a45dd6ea-4874-11ec-81d3-0242ac130003
name: Tomcat - Request from localhost IP address
description: |
'Detects request from localhost IP address.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
datatypes:
- Tomcat_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
TomcatEvent
| where SrcIpAddr == "127.0.0.1"
| extend File = extract(@'(.*\/)?(.*)', 2, tostring(UrlOriginal))
| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
TomcatEvent
Stage 2: where
| where SrcIpAddr == "127.0.0.1"
Stage 3: extend
| extend File = extract(@'(.*\/)?(.*)', 2, tostring(UrlOriginal))
Stage 4: extend
| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SrcIpAddr | eq |
| field:"src_ip" kind:eq value:"127.0.0.1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
File | extend |
FileCustomEntity | extend |
IPCustomEntity | extend |
UniFi Site Manager: Data Connector Health
#Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 1fb5c195-9bff-e18b-3e8f-5123f46bff9d
name: "UniFi Site Manager: Data Connector Health"
description: |
Identifies when UniFi Site Manager data connectors stop sending data, ensuring visibility into the UniFi estate is maintained.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Sites_CL
- Unifi_SiteManager_Devices_CL
- Unifi_SiteManager_Hosts_CL
- Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 15m
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
subTechniques:
- T1562.006
query: |
// UniFi Data Connector Health Monitoring
let StaleThreshold = 30;
let DevicesHealth = Unifi_SiteManager_Devices_CL
| where TimeGenerated > ago(2h)
| summarize LastReceived = max(TimeGenerated)
| extend TableName = "Unifi_SiteManager_Devices_CL", TableDisplay = "Devices";
let HostsHealth = Unifi_SiteManager_Hosts_CL
| where TimeGenerated > ago(2h)
| summarize LastReceived = max(TimeGenerated)
| extend TableName = "Unifi_SiteManager_Hosts_CL", TableDisplay = "Hosts";
let SitesHealth = Unifi_SiteManager_Sites_CL
| where TimeGenerated > ago(2h)
| summarize LastReceived = max(TimeGenerated)
| extend TableName = "Unifi_SiteManager_Sites_CL", TableDisplay = "Sites";
let ISPHealth = Unifi_SiteManager_ISPMetrics_CL
| where TimeGenerated > ago(2h)
| summarize LastReceived = max(TimeGenerated)
| extend TableName = "Unifi_SiteManager_ISPMetrics_CL", TableDisplay = "ISP Metrics";
union DevicesHealth, HostsHealth, SitesHealth, ISPHealth
| extend
MinutesSinceData = datetime_diff('minute', now(), LastReceived),
Status = case(
datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
"Critical"
)
| where MinutesSinceData >= StaleThreshold
| extend TimeGenerated = now()
| project
TimeGenerated,
TableName,
TableDisplay,
LastReceived,
MinutesSinceData,
Status
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: TableDisplay
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT4H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
Parameters
let StaleThreshold = 30;
Union (4 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: DevicesHealth, HostsHealth, SitesHealth, ISPHealth
Leg 1: DevicesHealth
Leg 2: HostsHealth
Leg 3: SitesHealth
Leg 4: ISPHealth
Applied to the Combined Result
| extend
MinutesSinceData = datetime_diff('minute', now(), LastReceived),
Status = case(
datetime_diff('minute', now(), LastReceived) <= 15, "Healthy",
datetime_diff('minute', now(), LastReceived) <= 30, "Delayed",
datetime_diff('minute', now(), LastReceived) <= 60, "Stale",
"Critical"
) | where MinutesSinceData >= StaleThreshold | extend TimeGenerated = now() | project
TimeGenerated,
TableName,
TableDisplay,
LastReceived,
MinutesSinceData,
Status
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
MinutesSinceData | ge |
| field:"MinutesSinceData" kind:ge value:"30" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
LastReceived | project |
MinutesSinceData | project |
Status | project |
TableDisplay | project |
TableName | project |
TimeGenerated | project |