Detection rules › Kusto
Kusto rules: 1v
Anomalous login followed by Teams action
#Detects anomalous IP address usage by user accounts and then checks to see if a suspicious Teams action is performed. Query calculates IP usage Delta for each user account and selects accounts where a delta >= 90% is observed between the most and least used IP. To further reduce results the query performs a prevalence check on the lowest used IP's country, only keeping IP's where the country is unusual for the tenant (dynamic ranges). Please note, if the initial logic of prevalence to find suspicious logon activity is noisy then consider adding filtering based on Location. Finally the user accounts activity within Teams logs is checked for suspicious commands (modifying user privileges or admin actions) during the period the suspicious IP was active.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- Office 365 Teams member removed (YARA-L)
Rule Body
id: 2b701288-b428-4fb8-805e-e4372c574786
name: Anomalous login followed by Teams action
description: |
'Detects anomalous IP address usage by user accounts and then checks to see if a suspicious Teams action is performed.
Query calculates IP usage Delta for each user account and selects accounts where a delta >= 90% is observed between the most and least used IP.
To further reduce results the query performs a prevalence check on the lowest used IP's country, only keeping IP's where the country is unusual for the tenant (dynamic ranges).
Please note, if the initial logic of prevalence to find suspicious logon activity is noisy then consider adding filtering based on Location.
Finally the user accounts activity within Teams logs is checked for suspicious commands (modifying user privileges or admin actions) during the period the suspicious IP was active.'
severity: Medium
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1199
- T1136
- T1078
- T1098
query: |
//The bigger the window the better the data sample size, as we use IP prevalence, more sample data is better.
//The minimum number of countries that the account has been accessed from [default: 2]
let minimumCountries = 2;
//The delta (%) between the largest in-use IP and the smallest [default: 95]
let deltaThreshold = 95;
//The maximum (%) threshold that the country appears in login data [default: 10]
let countryPrevalenceThreshold = 10;
//The time to project forward after the last login activity [default: 60min]
let projectedEndTime = 60m;
let queryfrequency = 1d;
let queryperiod = 14d;
let aadFunc = (tableName: string) {
// Get successful signins to Teams
let signinData =
table(tableName)
| where TimeGenerated > ago(queryperiod)
| where AppDisplayName has "Teams" and ConditionalAccessStatus =~ "success"
| extend Country = tostring(todynamic(LocationDetails)['countryOrRegion'])
| where isnotempty(Country) and isnotempty(IPAddress);
// Calculate prevalence of countries
let countryPrevalence =
signinData
| summarize CountCountrySignin = count() by Country
| extend TotalSignin = toscalar(signinData | summarize count())
| extend CountryPrevalence = toreal(CountCountrySignin) / toreal(TotalSignin) * 100;
// Count signins by user and IP address
let userIpSignin =
signinData
| summarize CountIPSignin = count(), Country = any(Country), ListSigninTimeGenerated = make_list(TimeGenerated) by IPAddress, UserPrincipalName;
// Calculate delta between the IP addresses with the most and minimum activity by user
let userIpDelta =
userIpSignin
| summarize MaxIPSignin = max(CountIPSignin), MinIPSignin = min(CountIPSignin), DistinctCountries = dcount(Country), make_set(Country) by UserPrincipalName
| extend UserIPDelta = toreal(MaxIPSignin - MinIPSignin) / toreal(MaxIPSignin) * 100;
// Collect Team operations the user account has performed within a time range of the suspicious signins
OfficeActivity
| where TimeGenerated > ago(queryfrequency)
| where Operation in~ ("TeamsAdminAction", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "AppInstalled", "BotAddedToTeam")
| where not (Operation in~ ("MemberAdded", "MemberRemoved") and CommunicationType in~ ("GroupChat", "OneonOne")) // These events have been noisy and are related to initiaing chat conversation and not admin operations.
| project OperationTimeGenerated = TimeGenerated, UserId = tolower(UserId), Operation
| join kind = inner(
userIpDelta
// Check users with activity from distinct countries
| where DistinctCountries >= minimumCountries
// Check users with high IP delta
| where UserIPDelta >= deltaThreshold
// Add information about signins and countries
| join kind = leftouter userIpSignin on UserPrincipalName
| join kind = leftouter countryPrevalence on Country
// Check activity that comes from nonprevalent countries
| where CountryPrevalence < countryPrevalenceThreshold
| project
UserPrincipalName,
SuspiciousIP = IPAddress,
UserIPDelta,
SuspiciousSigninCountry = Country,
SuspiciousCountryPrevalence = CountryPrevalence,
EventTimes = ListSigninTimeGenerated
) on $left.UserId == $right.UserPrincipalName
// Check the signins occured 60 min before the Teams operations
| mv-expand SigninTimeGenerated = EventTimes
| extend SigninTimeGenerated = todatetime(SigninTimeGenerated)
| where OperationTimeGenerated between (SigninTimeGenerated .. (SigninTimeGenerated + projectedEndTime))
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
| summarize arg_max(SigninTimeGenerated, *) by UserPrincipalName, SuspiciousIP, OperationTimeGenerated
| summarize
ActivitySummary = make_bag(pack(tostring(SigninTimeGenerated), pack("Operation", tostring(Operation), "OperationTime", OperationTimeGenerated)))
by UserPrincipalName, SuspiciousIP, SuspiciousSigninCountry, SuspiciousCountryPrevalence
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SuspiciousIP
version: 1.1.2
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Parameters
let minimumCountries = 2;
let deltaThreshold = 95;
let countryPrevalenceThreshold = 10;
let projectedEndTime = 60m;
let queryfrequency = 1d;
let queryperiod = 14d;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let Binding: aadFunc
let aadFunc = (tableName: string) {
let signinData =
table(tableName)
| where TimeGenerated > ago(queryperiod)
| where AppDisplayName has "Teams" and ConditionalAccessStatus =~ "success"
| extend Country = tostring(todynamic(LocationDetails)['countryOrRegion'])
| where isnotempty(Country) and isnotempty(IPAddress);
Let Binding: countryPrevalence
let countryPrevalence = signinData
| summarize CountCountrySignin = count() by Country
| extend TotalSignin = toscalar(signinData | summarize count())
| extend CountryPrevalence = toreal(CountCountrySignin) / toreal(TotalSignin) * 100;
Let Binding: userIpSignin
let userIpSignin = signinData
| summarize CountIPSignin = count(), Country = any(Country), ListSigninTimeGenerated = make_list(TimeGenerated) by IPAddress, UserPrincipalName;
Let Binding: userIpDelta
let userIpDelta = userIpSignin
| summarize MaxIPSignin = max(CountIPSignin), MinIPSignin = min(CountIPSignin), DistinctCountries = dcount(Country), make_set(Country) by UserPrincipalName
| extend UserIPDelta = toreal(MaxIPSignin - MinIPSignin) / toreal(MaxIPSignin) * 100;
Stage 1: union
union of 2 branches
Stage 2: source
OfficeActivity
Stage 3: where
where TimeGenerated > ago(86400s)
Stage 4: where
where Operation in~ ("AppInstalled", "BotAddedToTeam", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "TeamsAdminAction")
Stage 5: where
where not ((CommunicationType in~ ("GroupChat", "OneonOne") and Operation in~ ("MemberAdded", "MemberRemoved")))
Stage 6: project
project Operation, OperationTimeGenerated, UserId
Stage 7: join
join kind=inner (userIpDelta) on $left.UserId, $right.UserPrincipalName
Stage 8: mv-expand
mv-expand SigninTimeGenerated
Stage 9: extend
extend SigninTimeGenerated
Stage 10: where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h
where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h
Stage 11: source
OfficeActivity
Stage 12: where
where TimeGenerated > ago(86400s)
Stage 13: where
where Operation in~ ("AppInstalled", "BotAddedToTeam", "MemberAdded", "MemberRemoved", "MemberRoleChanged", "TeamsAdminAction")
Stage 14: where
where not ((CommunicationType in~ ("GroupChat", "OneonOne") and Operation in~ ("MemberAdded", "MemberRemoved")))
Stage 15: project
project Operation, OperationTimeGenerated, UserId
Stage 16: join
join kind=inner (userIpDelta) on $left.UserId, $right.UserPrincipalName
Stage 17: mv-expand
mv-expand SigninTimeGenerated
Stage 18: extend
extend SigninTimeGenerated
Stage 19: where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h
where OperationTimeGenerated - SigninTimeGenerated >= 0s AND OperationTimeGenerated - SigninTimeGenerated <= 1h
Stage 20: summarize
summarize by UserPrincipalName, SuspiciousIP, OperationTimeGenerated
Stage 21: summarize
summarize ActivitySummary by UserPrincipalName, SuspiciousIP, SuspiciousSigninCountry, SuspiciousCountryPrevalence
Stage 22: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CountryPrevalence | lt |
| field:"CountryPrevalence" kind:lt value:"10" |
DistinctCountries | ge |
| field:"DistinctCountries" kind:ge value:"2" |
Operation | in |
| field:"Operation" kind:in |
UserIPDelta | ge |
| field:"UserIPDelta" kind:ge value:"95" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
CommunicationType | in | GroupChat, OneonOne | excludes:CommunicationType field:"CommunicationType" value:"GroupChat" field:"CommunicationType" value:"OneonOne" |
Operation | in | MemberAdded, MemberRemoved | excludes:Operation field:"Operation" value:"MemberAdded" field:"Operation" value:"MemberRemoved" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivitySummary | summarize |
SuspiciousCountryPrevalence | summarize |
SuspiciousIP | summarize |
SuspiciousSigninCountry | summarize |
UserPrincipalName | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13 Privileges Against Tier Zero Group
#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 - Non Tier Zero Principals With ADCS ESC13 Privileges Against Tier Zero Group
Rule Body
id: 82c9f9b1-2990-4d5c-a5f4-26e4e54c904c
name: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13 Privileges
Against Tier Zero Group
description: BloodHound Attack Path Finding - Non Tier Zero Principals With ADCS ESC13
Privileges Against Tier Zero Group
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 == "T0ADCSESC13"
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 == "T0ADCSESC13"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC13" |
Copilot - Plugin Created by Non-Admin User
#'Detects when a normal user creates a Copilot plugin. This can be used to inject malicious prompts, tools, or data exfiltration paths. This rule identifies potential persistence or privilege misuse scenarios where non-administrative users create plugins that could be leveraged for malicious purposes.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Rule Body
id: a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6
name: Copilot - Plugin Created by Non-Admin User
description: |
'Detects when a normal user creates a Copilot plugin. This can be used to inject malicious prompts, tools, or data exfiltration paths.
This rule identifies potential persistence or privilege misuse scenarios where non-administrative users create plugins that could be leveraged for malicious purposes.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftCopilot
dataTypes:
- CopilotActivity
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1546
- T1098
query: |
CopilotActivity
| where RecordType == "CreateCopilotPlugin"
| where ActorUserType != "Admin"
| extend Data = parse_json(LLMEventData)
| extend Plugin = tostring(Data.Resource[0].Property)
| project TimeGenerated, ActorName, ActorUserId, SrcIpAddr, Plugin
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CopilotActivity
Stage 2: where
| where RecordType == "CreateCopilotPlugin"
Stage 3: where
| where ActorUserType != "Admin"
Stage 4: extend
| extend Data = parse_json(LLMEventData)
Stage 5: extend
| extend Plugin = tostring(Data.Resource[0].Property)
Stage 6: project
| project TimeGenerated, ActorName, ActorUserId, SrcIpAddr, Plugin
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActorUserType | ne |
| field:"ActorUserType" kind:ne value:"Admin" |
RecordType | eq |
| field:"RecordType" kind:eq value:"CreateCopilotPlugin" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorName | project |
ActorUserId | project |
Plugin | project |
SrcIpAddr | project |
TimeGenerated | project |
Cortex XDR Incident - Low
#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.
'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '
Rule Body
id: 1426bbcf-a9ae-4aa5-9da6-abbf48f04115
name: Cortex XDR Incident - Low
description: |
'A new incident was created in the Cortex XDR portal with a severity "Low". Click on the events for incident details. '
severity: Low
requiredDataConnectors:
- connectorId: CortexXDR
dataTypes:
- CortexXDR_Incidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride: null
customDetails: null
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: users_s
- entityType: URL
fieldMappings:
- identifier: Url
columnName: xdr_url_s
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: hosts_s
sentinelEntitiesMappings: null
query: |
CortexXDR_Incidents_CL
| where severity_s contains "low"
| where status_s !contains "resolved_auto"
// the below line filters for incidents that has at least one user or one host in it. Comment the below line if you want to generated incidents that have 0 user or 0 host as well
| where host_count_d > 0 or user_count_d > 0
| project TimeGenerated, incident_id_s, status_s, severity_s, incident_sources_s, description_s, xdr_url_s, hosts_s, users_s, alert_categories_s, host_count_d, user_count_d, alert_count_d, creation_time_d, modification_time_d
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CortexXDR_Incidents_CL
Stage 2: where
| where severity_s contains "low"
Stage 3: where
| where status_s !contains "resolved_auto"
Stage 4: where
| where host_count_d > 0 or user_count_d > 0
Stage 5: project
| project TimeGenerated, incident_id_s, status_s, severity_s, incident_sources_s, description_s, xdr_url_s, hosts_s, users_s, alert_categories_s, host_count_d, user_count_d, alert_count_d, creation_time_d, modification_time_d
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
host_count_d | gt |
| field:"host_count_d" kind:gt value:"0" |
severity_s | contains |
| field:"okta::severity" kind:contains value:"low" |
user_count_d | gt |
| field:"user_count_d" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
status_s | contains | resolved_auto | excludes:status_s field:"status_s" value:"resolved_auto" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
TimeGenerated | project |
alert_categories_s | project |
alert_count_d | project |
creation_time_d | project |
description_s | project |
host_count_d | project |
hosts_s | project |
incident_id_s | project |
incident_sources_s | project |
modification_time_d | project |
severity_s | project |
status_s | project |
user_count_d | project |
users_s | project |
xdr_url_s | project |
Cyble Vision Alerts Cyble Web Applications
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'Creates an incident for each discovered or exposed web application detected by Cyble Intelligence. Useful for SOC teams to investigate externally facing login portals, misconfigurations, and exposed infrastructure.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 359ddb25-eab1-4ef5-8303-ed3a9b680690
name: Cyble Vision Alerts Cyble Web Applications
description: |
'Creates an incident for each discovered or exposed web application detected by Cyble Intelligence. Useful for SOC teams to investigate externally facing login portals, misconfigurations, and exposed infrastructure.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: PT5M
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- Reconnaissance
relevantTechniques:
- T1595
- T1592
query: |
Alerts_web_applications
| where Service == "web_applications"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: Cyble Web Application Found {{WA_Title_Nested}}
alertDescriptionFormat: |
A web application was detected by Cyble Intelligence.
Host {{WA_Host_Nested}}
Port {{WA_Port_Nested}}
URL {{WA_URL}}
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
Host: WA_Host_Nested
DetectedURL: WA_URL
Title: WA_Title_Nested
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: WA_URL
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_web_applications
Stage 2: where
| where Service == "web_applications"
Stage 3: extend
| extend MappedSeverity = Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"web_applications" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
Cyble Vision Alerts Docker
#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 Docker Hub container entries related to monitored keywords. Uses Alerts_docker parser. Includes metadata such as developer, stars, downloads, and image URL. Raises one incident per alert.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Discovery | |
| Exfiltration |
Rule Body
id: 47dee28d-fa74-49cd-b5fb-397b047a73c0
name: Cyble Vision Alerts Docker
description: |
'Detects Docker Hub container entries related to monitored keywords. Uses Alerts_docker parser. Includes metadata such as developer, stars, downloads, and image URL. Raises one incident per alert.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- Exfiltration
- Execution
- Discovery
relevantTechniques:
- T1537
- T1610
- T1083
query: |
Alerts_docker
| where Service == "docker"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: Docker Image Identified {{DK_Title}}
alertDescriptionFormat: |
A Docker Hub image related to monitored entity {{KeywordName}} was discovered. Developer {{DK_Developer}}. Downloads {{DK_Downloads}}. Review metadata and verify if the image poses supply chain or impersonation risk.
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
DK_Title: DK_Title
DK_description: DK_Description
DK_Developer: DK_Developer
DK_DeveloperUrl: DK_DeveloperUrl
DK_Downloads: DK_Downloads
DK_Stars: DK_Stars
DK_Url: DK_Url
entityMappings:
- entityType: Url
fieldMappings:
- identifier: Url
columnName: DK_Url
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: KeywordName
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_docker
Stage 2: where
| where Service == "docker"
Stage 3: extend
| extend MappedSeverity = Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"docker" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
Darktrace AI Analyst (Legacy)
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'This rule creates Microsoft Sentinel Incidents based on Darktrace AI Analyst Incidents, fetched every 5 minutes.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Lateral Movement | |
| Command & Control |
Rule Body
id: ffa2977f-3077-4bba-b1bf-f3417699cbb0
name: Darktrace AI Analyst (Legacy)
description: |
'This rule creates Microsoft Sentinel Incidents based on Darktrace AI Analyst Incidents, fetched every 5 minutes.'
severity: High
requiredDataConnectors:
- connectorId: DarktraceRESTConnector
dataTypes:
- darktrace_model_alerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
- LateralMovement
- CommandAndControl
relevantTechniques:
- T1190
- T1059
- T1021
- T1071
query: |
darktrace_model_alerts_CL
| where dtProduct_s =="AI Analyst"
| project-rename EventStartTime=startTime_s, EventEndTime = endTime_s, NetworkRuleName=title_s, DtCurrentGroup=externalId_g, ThreatCategory=dtProduct_s, ThreatRiskLevel=score_d, SrcHostname=hostname_s, SrcIpAddr=deviceIP_s, DtURL=url_s, DtSummary=summary_s, DtGroupScore=groupScore_d, DtGroupCategory=groupCategory_s, DtSrcDeviceName=bestDeviceName_s, DtIndentifier=identifier_s, ActivityID=activityId_s, DtGroupingID=groupingId_s, DtGroupByActivity=groupByActivity_b, DtSummaryFirstSentence=summaryFirstSentence_s, DtNewEvent=newEvent_b, DtCGLegacy=currentGroup_s, DtGroupPreviousGroups=groupPreviousGroups_s, DtTime=time_s, DtSeverity=Severity, DtLongitude=longitude_d, DtLatitude=latitude_d
| extend EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", DtSentinelCategory=DtGroupCategory
| extend DtSentinelCategory = case (DtGroupCategory == "compliance", "Low",
DtGroupCategory == "suspicious", "Medium",
"High") //compliance -> low, suspcious -> medium, critical -> high
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SrcHostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
customDetails:
EventStartTime: EventStartTime
EventEndTime: EventEndTime
NetworkRuleName: NetworkRuleName
DtCurrentGroup: DtCurrentGroup
ThreatRiskLevel: ThreatRiskLevel
DtSummary: DtSummary
DtGroupScore: DtGroupScore
DtGroupCategory: DtGroupCategory
DtSentinelCategory: DtSentinelCategory
DtSrcDeviceName: DtSrcDeviceName
DtNewEvent: DtNewEvent
DtSeverity: DtSeverity
alertDetailsOverride:
alertDisplayNameFormat: 'Darktrace: {{ThreatRiskLevel}} - {{NetworkRuleName}}'
alertDescriptionFormat: '{{DtSummary}}'
alertTacticsColumnName: # leave empty
alertSeverityColumnName: # leave empty
alertDynamicProperties:
- alertProperty: AlertLink
value: DtURL
- alertProperty: ProviderName
value: EventVendor
- alertProperty: ProductName
value: EventProduct
- alertProperty: ProductComponentName
value: ThreatCategory
- alertProperty: Severity
value: DtSentinelCategory
version: 1.1.1
kind: NRT
Stages and Predicates
Stage 1: source
darktrace_model_alerts_CL
Stage 2: where
| where dtProduct_s =="AI Analyst"
Stage 3: project-rename
| project-rename EventStartTime=startTime_s, EventEndTime = endTime_s, NetworkRuleName=title_s, DtCurrentGroup=externalId_g, ThreatCategory=dtProduct_s, ThreatRiskLevel=score_d, SrcHostname=hostname_s, SrcIpAddr=deviceIP_s, DtURL=url_s, DtSummary=summary_s, DtGroupScore=groupScore_d, DtGroupCategory=groupCategory_s, DtSrcDeviceName=bestDeviceName_s, DtIndentifier=identifier_s, ActivityID=activityId_s, DtGroupingID=groupingId_s, DtGroupByActivity=groupByActivity_b, DtSummaryFirstSentence=summaryFirstSentence_s, DtNewEvent=newEvent_b, DtCGLegacy=currentGroup_s, DtGroupPreviousGroups=groupPreviousGroups_s, DtTime=time_s, DtSeverity=Severity, DtLongitude=longitude_d, DtLatitude=latitude_d
Stage 4: extend
| extend EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", DtSentinelCategory=DtGroupCategory
Stage 5: extend
| extend DtSentinelCategory = case (DtGroupCategory == "compliance", "Low",
DtGroupCategory == "suspicious", "Medium",
"High")
DtSentinelCategory =DtGroupCategory == "compliance""Low"DtGroupCategory == "suspicious""Medium""High"Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
dtProduct_s | eq |
| field:"dtProduct_s" kind:eq value:"AI Analyst" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityID | project-rename |
DtCGLegacy | project-rename |
DtCurrentGroup | project-rename |
DtGroupByActivity | project-rename |
DtGroupCategory | project-rename |
DtGroupPreviousGroups | project-rename |
DtGroupScore | project-rename |
DtGroupingID | project-rename |
DtIndentifier | project-rename |
DtLatitude | project-rename |
DtLongitude | project-rename |
DtNewEvent | project-rename |
DtSeverity | project-rename |
DtSrcDeviceName | project-rename |
DtSummary | project-rename |
DtSummaryFirstSentence | project-rename |
DtTime | project-rename |
DtURL | project-rename |
EventEndTime | project-rename |
EventStartTime | project-rename |
NetworkRuleName | project-rename |
SrcHostname | project-rename |
SrcIpAddr | project-rename |
ThreatCategory | project-rename |
ThreatRiskLevel | project-rename |
DtSentinelCategory | extend |
EventProduct | extend |
EventVendor | extend |
Detect Suspicious Commands Initiated by Webserver Processes
#This query was originally published in the threat analytics report, Operation Soft Cell. Operation Soft Cell is a series of campaigns targeting users' call logs at telecommunications providers throughout the world. These attacks date from as early as 2012. Operation Soft Cell operators sometimes use legitimate web server processes to launch commands, especially for network discovery and user/owner discovery. The following query detects activity of this kind. Reference - https://www.cybereason.com/blog/operation-soft-cell-a-worldwide-campaign-against-telecommunications-providers
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Stealth | |
| Discovery |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: fa2f7d8a-6726-465a-aa72-6f6e3d4c99d7
name: Detect Suspicious Commands Initiated by Webserver Processes
description: |
This query was originally published in the threat analytics report, Operation Soft Cell.
Operation Soft Cell is a series of campaigns targeting users' call logs at telecommunications providers throughout the world. These attacks date from as early as 2012.
Operation Soft Cell operators sometimes use legitimate web server processes to launch commands, especially for network discovery and user/owner discovery. The following query detects activity of this kind.
Reference - https://www.cybereason.com/blog/operation-soft-cell-a-worldwide-campaign-against-telecommunications-providers
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- DefenseEvasion
- Discovery
relevantTechniques:
- T1059
- T1574
- T1087
- T1082
tags:
- Operation Soft Cell
- Webserver Process
- Discovery
query: |
// Suspicious commands launched by web server processes
DeviceProcessEvents
| where (((InitiatingProcessParentFileName in("w3wp.exe", "beasvc.exe",
"httpd.exe") or InitiatingProcessParentFileName startswith "tomcat")
or InitiatingProcessFileName in("w3wp.exe", "beasvc.exe", "httpd.exe") or
InitiatingProcessFileName startswith "tomcat"))
and FileName in~('cmd.exe', 'powershell.exe')
| where ProcessCommandLine contains '%temp%'
or ProcessCommandLine has 'wget'
or ProcessCommandLine has 'whoami'
or ProcessCommandLine has 'certutil'
or ProcessCommandLine has 'systeminfo'
or ProcessCommandLine has 'ping'
or ProcessCommandLine has 'ipconfig'
or ProcessCommandLine has 'timeout'
| summarize
take_any(FileName),
make_set(ProcessCommandLine, 100000),
take_any(InitiatingProcessFileName),
take_any(InitiatingProcessParentFileName)
by DeviceId, DeviceName
| 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 (((InitiatingProcessParentFileName in("w3wp.exe", "beasvc.exe",
"httpd.exe") or InitiatingProcessParentFileName startswith "tomcat")
or InitiatingProcessFileName in("w3wp.exe", "beasvc.exe", "httpd.exe") or
InitiatingProcessFileName startswith "tomcat"))
and FileName in~('cmd.exe', 'powershell.exe')
Stage 3: where
| where ProcessCommandLine contains '%temp%'
or ProcessCommandLine has 'wget'
or ProcessCommandLine has 'whoami'
or ProcessCommandLine has 'certutil'
or ProcessCommandLine has 'systeminfo'
or ProcessCommandLine has 'ping'
or ProcessCommandLine has 'ipconfig'
or ProcessCommandLine has 'timeout'
Stage 4: summarize
| summarize
take_any(FileName),
make_set(ProcessCommandLine, 100000),
take_any(InitiatingProcessFileName),
take_any(InitiatingProcessParentFileName)
by DeviceId, DeviceName
Stage 5: extend
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 6: 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 | in |
| field:"process_basename" kind:in |
InitiatingProcessFileName | in |
| field:"parent_process_basename" kind:in |
InitiatingProcessFileName | starts_with |
| field:"parent_process_basename" kind:starts_with value:"tomcat" |
InitiatingProcessParentFileName | in |
| field:"grandparent_process_basename" kind:in |
InitiatingProcessParentFileName | starts_with |
| field:"grandparent_process_basename" kind:starts_with value:"tomcat" |
ProcessCommandLine | contains |
| field:"CommandLine" kind:contains value:"%temp%" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
DeviceName | summarize |
HostName | extend |
DnsDomain | extend |
First access credential added to Application or Service Principal where no credential was present
#This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated. If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential. Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule Body
id: 2cfc3c6e-f424-4b88-9cc9-c89f482d016a
name: First access credential added to Application or Service Principal where no credential was present
description: |
'This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated.
If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential.
Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1550.001
tags:
- Solorigate
- NOBELIUM
query: |
AuditLogs
| where OperationName has ("Certificates and secrets management")
| where Result =~ "success"
| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Application"
| extend targetDisplayName = tostring(TargetResource.displayName),
targetId = tostring(TargetResource.id),
targetType = tostring(TargetResource.type),
keyEvents = TargetResource.modifiedProperties
)
| mv-apply Property = keyEvents on
(
where Property.displayName =~ "KeyDescription"
| extend new_value_set = parse_json(tostring(Property.newValue)),
old_value_set = parse_json(tostring(Property.oldValue))
)
| where old_value_set == "[]"
| mv-expand new_value_set
| parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
| where keyUsage =~ "Verify"
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend InitiatingUserAgent = tostring(AdditionalDetail.value)
)
| project-away new_value_set, old_value_set, TargetResource, Property, AdditionalDetail
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent,
targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
| extend Name = split(InitiatingUserPrincipalName, "@")[0], UPNSuffix = split(InitiatingUserPrincipalName, "@")[1]
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: targetDisplayName
version: 1.1.5
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName has ("Certificates and secrets management")
Stage 3: where
| where Result =~ "success"
Stage 4: where
| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
Stage 5: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Application"
| extend targetDisplayName = tostring(TargetResource.displayName),
targetId = tostring(TargetResource.id),
targetType = tostring(TargetResource.type),
keyEvents = TargetResource.modifiedProperties
)
Stage 6: kusto:mv-apply
| mv-apply Property = keyEvents on
(
where Property.displayName =~ "KeyDescription"
| extend new_value_set = parse_json(tostring(Property.newValue)),
old_value_set = parse_json(tostring(Property.oldValue))
)
Stage 7: where
| where old_value_set == "[]"
Stage 8: mv-expand
| mv-expand new_value_set
Stage 9: parse
| parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
Stage 10: where
| where keyUsage =~ "Verify"
Stage 11: kusto:mv-apply
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend InitiatingUserAgent = tostring(AdditionalDetail.value)
)
Stage 12: project-away
| project-away new_value_set, old_value_set, TargetResource, Property, AdditionalDetail
Stage 13: extend (5 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
Stage 14: project-reorder
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserAgent,
targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
Stage 15: extend
| extend Name = split(InitiatingUserPrincipalName, "@")[0], UPNSuffix = split(InitiatingUserPrincipalName, "@")[1]
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
Name | extend |
UPNSuffix | extend |
GitHub - User was added to the organization
#Detect activities when a user was added to the organization. This query runs every day and its severity is Medium.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GitHub | Audit log action org.add_member: User joined organization |
Rules Detecting the Same Action
These rules filter on the same operation.
- GitHub User Added or Removed from Org (Panther)
- New Github Organization Member Added (Sigma)
- New GitHub Owner Added (Elastic)
- New User Added To GitHub Organization (Elastic)
Rule Body
id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c4
name: GitHub - User was added to the organization
description: |
'Detect activities when a user was added to the organization. This query runs every day and its severity is Medium.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
GitHubAuditData
| where Action == "org.add_member"
| project Actor, Action
| extend Name = iif(Actor contains "@", split(Actor, "@")[0], Actor)
| extend UPNSuffix = iif(Actor contains "@", split(Actor, "@")[1], "")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Actor
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
GitHubAuditData
Stage 2: where
| where Action == "org.add_member"
Stage 3: project
| project Actor, Action
Stage 4: extend
| extend Name = iif(Actor contains "@", split(Actor, "@")[0], Actor)
Stage 5: extend
| extend UPNSuffix = iif(Actor contains "@", split(Actor, "@")[1], "")
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"org.add_member" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
Actor | project |
Name | extend |
UPNSuffix | extend |
MFA Spamming followed by Successful login
#Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window. Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Sign-in recording an authentication requirement |
Rule Body
id: a8cc6d5c-4e7e-4b48-b4ac-d8a116c62a8b
name: MFA Spamming followed by Successful login
description: |
'Identifies MFA Spamming followed by Successful logins and by a successful authentication within a given time window.
Default Failure count is 10 and 1 successful login with default Time Window is 5 minutes.'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
// Filter for sign-in logs ingested within the last day
SigninLogs
| where ingestion_time() > ago(1d)
// Filter for records with AuthenticationRequirement set to multiFactorAuthentication
| where AuthenticationRequirement == "multiFactorAuthentication"
// Extract information from dynamic columns DeviceDetail and LocationDetails
| extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
// Extract specific attributes from DeviceDetail and LocationDetails
| extend
OS = tostring(DeviceDetail.operatingSystem),
Browser = tostring(DeviceDetail.browser),
State = tostring(LocationDetails.state),
City = tostring(LocationDetails.city),
Region = tostring(LocationDetails.countryOrRegion)
// Expand multi-value property AuthenticationDetails into separate records
| mv-expand todynamic(AuthenticationDetails)
// Parse AuthResult from JSON in AuthenticationDetails and convert to string
| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
// Summarize data by aggregating statistics for each user, IP, and AuthResult
| summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region
// Calculate AuthenticationWindow by finding time difference between start and end times
| extend AuthenticationWindow = (EndTime - StartTime)
// Filter for records with more than 10 failed attempts in 5-minute window and at least 1 successful attempt
| where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1
// Extract user's name and UPN suffix using split function
| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
SigninLogs
Stage 2: where
| where ingestion_time() > ago(1d)
Stage 3: where
| where AuthenticationRequirement == "multiFactorAuthentication"
Stage 4: extend
| extend DeviceDetail = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
Stage 5: extend
| extend
OS = tostring(DeviceDetail.operatingSystem),
Browser = tostring(DeviceDetail.browser),
State = tostring(LocationDetails.state),
City = tostring(LocationDetails.city),
Region = tostring(LocationDetails.countryOrRegion)
Stage 6: mv-expand
| mv-expand todynamic(AuthenticationDetails)
Stage 7: extend
| extend AuthResult = tostring(parse_json(AuthenticationDetails).authenticationStepResultDetail)
Stage 8: summarize
| summarize FailedAttempts = countif(AuthResult == "MFA denied; user declined the authentication" or AuthResult == "MFA denied; user did not respond to mobile app notification"), SuccessfulAttempts = countif(AuthResult == "MFA successfully completed"), InvolvedOS = make_set(OS, 5), InvolvedBrowser = make_set(Browser), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, IPAddress, State, City, Region
Stage 9: extend
| extend AuthenticationWindow = (EndTime - StartTime)
Stage 10: where
| where FailedAttempts > 10 and AuthenticationWindow <= 5m and SuccessfulAttempts >= 1
Stage 11: extend
| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
City | summarize |
EndTime | summarize |
FailedAttempts | summarize |
IPAddress | summarize |
InvolvedBrowser | summarize |
InvolvedOS | summarize |
Region | summarize |
StartTime | summarize |
State | summarize |
SuccessfulAttempts | summarize |
UserPrincipalName | summarize |
AuthenticationWindow | extend |
Name | extend |
UPNSuffix | extend |
NordPass - User deletes items in bulk
#This will alert you if a user deletes items in bulk, namely, more than 10 items or in the span of 10 minutes. If a mix of bulk and one-off deletions were performed, this will group all actions and report the total number of items deleted.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Impact |
Rule Body
id: f72f630f-c890-49fe-b747-80f4fb3b6348
name: NordPass - User deletes items in bulk
kind: Scheduled
version: 1.0.0
displayName: User deletes items in bulk
description: |
This will alert you if a user deletes items in bulk, namely, more than 10 items or in the span of 10 minutes.
If a mix of bulk and one-off deletions were performed, this will group all actions and report the total number of items deleted.
severity: High
query: |
NordPassEventLogs_CL
| where action == "items_deleted" and event_type == "item_delete"
| extend item_count = array_length(todynamic(metadata).items)
| summarize total_items = sum(item_count) by user_email
| where total_items >= 10
requiredDataConnectors:
- connectorId: NordPass
dataTypes:
- NordPassEventLogs_CL
queryFrequency: 5m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
- Collection
relevantTechniques:
- T1485
- T1074
incidentConfiguration:
createIncident: false
customDetails:
ItemCount: total_items
User: user_email
entityMappings:
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: user_email
Stages and Predicates
Stage 1: source
NordPassEventLogs_CL
Stage 2: where
| where action == "items_deleted" and event_type == "item_delete"
Stage 3: extend
| extend item_count = array_length(todynamic(metadata).items)
Stage 4: summarize
| summarize total_items = sum(item_count) by user_email
Stage 5: where
| where total_items >= 10
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"items_deleted" |
event_type | eq |
| field:"event_type" kind:eq value:"item_delete" |
total_items | ge |
| field:"total_items" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
total_items | summarize |
user_email | summarize |
Port scan detected (ASIM Network Session schema)
#This rule identifies a possible port scan, in which a single source tries to access a large number of different ports is a short time frame. This may indicate that a port scanner is trying to identify open ports in order to penetrate a system. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM NetworkSession schema
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry Coverage
Rule Body
id: 1da9853f-3dea-4ea9-b7e5-26730da3d537
name: Port scan detected (ASIM Network Session schema)
description: |
'This rule identifies a possible port scan, in which a single source tries to access a large number of different ports is a short time frame. This may indicate that a [port scanner](https://en.wikipedia.org/wiki/Port_scanner) is trying to identify open ports in order to penetrate a system.
This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM NetworkSession schema'
alertDetailsOverride:
alertDisplayNameFormat: Potential port scan from {{SrcIpAddr}}
alertDescriptionFormat: A port scan has been performed from address {{SrcIpAddr}} over {{AttemptedPortsCount}} ports within 5 minutes. This may indicate that a [port scanner](https://en.wikipedia.org/wiki/Port_scanner) is trying to identify open ports in order to penetrate a system.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWSS3
dataTypes:
- AWSVPCFlow
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: MicrosoftSysmonForLinux
dataTypes:
- Syslog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: AzureMonitor(VMInsights)
dataTypes:
- VMConnection
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- connectorId: AzureNSG
dataTypes:
- AzureDiagnostics
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: CiscoAsaAma
dataTypes:
- CommonSecurityLog
- connectorId: Corelight
dataTypes:
- Corelight_CL
- connectorId: AIVectraStream
dataTypes:
- VectraStream
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: CiscoMeraki
dataTypes:
- Syslog
- CiscoMerakiNativePoller
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1046
tags:
- ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Sophos%20XG%20Firewall/Analytic%20Rules/PortScanDetected.yaml
version: 1.0.0
- Schema: ASimNetworkSessions
SchemaVersion: 0.2.4
query: |
let PortScanThreshold = 50;
_Im_NetworkSession
| where ipv4_is_private(SrcIpAddr) == False
| where SrcIpAddr !in ("127.0.0.1", "::1")
| summarize AttemptedPortsCount=dcount(DstPortNumber), AttemptedPorts=make_set(DstPortNumber, 100), ReportedBy=make_set(strcat(EventVendor, "/", EventProduct), 20) by SrcIpAddr, bin(TimeGenerated, 5m)
| where AttemptedPortsCount > PortScanThreshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
customDetails:
AttemptedPortsCount: AttemptedPortsCount
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let PortScanThreshold = 50;
Stage 1: source
_Im_NetworkSession
Stage 2: where
| where ipv4_is_private(SrcIpAddr) == False
Stage 3: where
| where SrcIpAddr !in ("127.0.0.1", "::1")
Stage 4: summarize
| summarize AttemptedPortsCount=dcount(DstPortNumber), AttemptedPorts=make_set(DstPortNumber, 100), ReportedBy=make_set(strcat(EventVendor, "/", EventProduct), 20) by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 5: where
| where AttemptedPortsCount > PortScanThreshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AttemptedPortsCount | gt |
| field:"AttemptedPortsCount" kind:gt value:"50" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | 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:SrcIpAddr |
SrcIpAddr | in | 127.0.0.1, ::1 | excludes:SrcIpAddr field:"SrcIpAddr" value:"127.0.0.1" field:"SrcIpAddr" value:"::1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AttemptedPorts | summarize |
AttemptedPortsCount | summarize |
ReportedBy | summarize |
SrcIpAddr | summarize |
Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Anomaly based (ASIM DNS Solution)
#This rule makes use of the series decompose anomaly method to detect clients with a high NXDomain response count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when new IP address DNS activity is identified as an outlier when compared to the baseline, indicating a recurring pattern. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 01191239-274e-43c9-b154-3a042692af06
name: Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Anomaly based (ASIM DNS Solution)
description: |
'This rule makes use of the series decompose anomaly method to detect clients with a high NXDomain response count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when new IP address DNS activity is identified as an outlier when compared to the baseline, indicating a recurring pattern. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available
tags:
- Schema: ASimDns
SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1568
- T1008
query: |
let threshold = 2.5;
let min_t = ago(14d);
let max_t = now();
let timeframe = 1d;
// calculate avg. eps(events per second)
let eps = materialize (_Im_Dns
| project TimeGenerated
| where TimeGenerated > ago(5m)
| count
| extend Count = Count / 300);
let maxSummarizedTime = toscalar (
union isfuzzy=true
(
DNS_Summarized_Logs_ip_CL
| where EventTime_t >= min_t
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('hour', 1, max_TimeGenerated)
),
(
print(min_t)
| project max_TimeGenerated = print_0
)
| summarize maxTimeGenerated = max(max_TimeGenerated)
);
let summarizationexist = materialize(
union isfuzzy=true
(
DNS_Summarized_Logs_ip_CL
| where EventTime_t > ago(1d)
| project v = int(2)
),
(
print int(1)
| project v = print_0
)
| summarize maxv = max(v)
| extend sumexist = (maxv > 1)
);
let allData = union isfuzzy=true
(
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) > 1000
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(2d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
),
(
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) between (501 .. 1000)
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(3d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
),
(
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) <= 500
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(4d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
),
(
DNS_Summarized_Logs_ip_CL
| where EventTime_t > min_t and EventResultDetails_s == 'NXDOMAIN'
| project-rename
SrcIpAddr=SrcIpAddr_s,
DnsQuery=DnsQuery_s,
Count=count__d,
EventTime=EventTime_t
| extend Count = toint(Count)
);
allData
| make-series QueryCount=dcount(DnsQuery) on EventTime from min_t to max_t step timeframe by SrcIpAddr
// include calculated Anomalies, Score and Baseline
| extend (anomalies, score, baseline) = series_decompose_anomalies(QueryCount, threshold, -1, 'linefit')
| mv-expand anomalies, score, baseline, EventTime, QueryCount
| extend
anomalies = toint(anomalies),
score = toint(score),
baseline = toint(baseline),
EventTime = todatetime(EventTime),
Total = tolong(QueryCount)
| where EventTime >= ago(timeframe)
| where score >= threshold * 2
// Join allData to include DnsQuery details
| join kind=inner(allData
| where TimeGenerated >= ago(timeframe)
| summarize DNSQueries = make_set(DnsQuery, 1000) by SrcIpAddr)
on SrcIpAddr
| project-away SrcIpAddr1
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
DNSQueries: DNSQueries
AnomalyScore: score
baseline: baseline
Total: Total
alertDetailsOverride:
alertDisplayNameFormat: "[Anomaly] Potential DGA (Domain Generation Algorithm) originating from client IP: '{{SrcIpAddr}}' has been detected."
alertDescriptionFormat: "Client has been identified with high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). This client is found to be communicating with multiple Domains which do not exist.\n\nBaseline Domain or DNS query count from this client: '{{baseline}}'\n\nCurrent Domain or DNS query count from this client: '{{Total}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 2.5;
let min_t = ago(14d);
let max_t = now();
let timeframe = 1d;
Let Binding: eps
let eps = materialize (_Im_Dns
| project TimeGenerated
| where TimeGenerated > ago(5m)
| count
| extend Count = Count / 300);
Let Binding: maxSummarizedTime
let maxSummarizedTime = toscalar (
union isfuzzy=true
(
DNS_Summarized_Logs_ip_CL
| where EventTime_t >= min_t
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('hour', 1, max_TimeGenerated)
),
(
print(min_t)
| project max_TimeGenerated = print_0
)
| summarize maxTimeGenerated = max(max_TimeGenerated)
);
Let Binding: summarizationexist
let summarizationexist = materialize(
union isfuzzy=true
(
DNS_Summarized_Logs_ip_CL
| where EventTime_t > ago(1d)
| project v = int(2)
),
(
print int(1)
| project v = print_0
)
| summarize maxv = max(v)
| extend sumexist = (maxv > 1)
);
Union isfuzzy=true (4 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: datatable(exists:, datatable(exists:, datatable(exists:, DNS_Summarized_Logs_ip_CL
Leg 1: datatable(exists:
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) > 1000
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(2d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
Leg 2: datatable(exists:
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) between (501 .. 1000)
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(3d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
Leg 3: datatable(exists:
(datatable(exists: int, sumexist: bool)[1, false]
| where toscalar(eps) <= 500
| join (summarizationexist) on sumexist)
| join (
_Im_Dns(responsecodename='NXDOMAIN', starttime=todatetime(ago(4d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated, 1h)
| extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
)
on exists
| project-away exists, maxv, sum*
Leg 4: DNS_Summarized_Logs_ip_CL
DNS_Summarized_Logs_ip_CL
| where EventTime_t > min_t and EventResultDetails_s == 'NXDOMAIN'
| project-rename
SrcIpAddr=SrcIpAddr_s,
DnsQuery=DnsQuery_s,
Count=count__d,
EventTime=EventTime_t
| extend Count = toint(Count)
Applied to the Combined Result
| make-series QueryCount=dcount(DnsQuery) on EventTime from min_t to max_t step timeframe by SrcIpAddr | extend (anomalies, score, baseline) = series_decompose_anomalies(QueryCount, threshold, -1, 'linefit') | mv-expand anomalies, score, baseline, EventTime, QueryCount | extend
anomalies = toint(anomalies),
score = toint(score),
baseline = toint(baseline),
EventTime = todatetime(EventTime),
Total = tolong(QueryCount) | where EventTime >= ago(timeframe) | where score >= threshold * 2 | join kind=inner(allData
| where TimeGenerated >= ago(timeframe)
| summarize DNSQueries = make_set(DnsQuery, 1000) by SrcIpAddr)
on SrcIpAddr | project-away SrcIpAddr1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResultDetails_s | eq |
| field:"EventResultDetails_s" kind:eq value:"NXDOMAIN" |
score | ge |
| field:"score" kind:ge value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
QueryCount | summarize |
SrcIpAddr | summarize |
anomalies | extend |
baseline | extend |
score | extend |
EventTime | extend |
Total | extend |
Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Static threshold based (ASIM DNS Solution)
#This rule identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 89ba52fa-96a7-4653-829a-ca49bb13336c
name: Potential DGA(Domain Generation Algorithm) detected via Repetitive Failures - Static threshold based (ASIM DNS Solution)
description: |
'This rule identifies clients with a high NXDomain count, which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). An alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available
tags:
- Schema: ASimDns
SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1568
- T1008
query: |
let threshold = 100;
let lookback = 10d;
let referenceendtime = 1d;
let nxDomainDnsEvents = (stime: datetime, etime: datetime) {
_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
| where DnsQueryTypeName in ("A", "AAAA")
| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
| where DnsQuery !contains "/" and DnsQuery contains "."
};
nxDomainDnsEvents (stime=ago(referenceendtime), etime=now())
| summarize
StartTimeUtc = min(TimeGenerated),
EndTimeUtc = max(TimeGenerated),
DNSQueryCount=dcount(DnsQuery)
by SrcIpAddr
| where DNSQueryCount > threshold
// Filter out previously seen IPs
| join kind=leftanti (nxDomainDnsEvents (stime=ago(lookback), etime=ago(referenceendtime))
| summarize DNSQueryCount=dcount(DnsQuery) by SrcIpAddr, bin(TimeGenerated,1d)
| where DNSQueryCount > threshold)
on SrcIpAddr
// Pull out sample NXDomain responses for those remaining potentially infected IPs
| join kind = inner (nxDomainDnsEvents (stime=ago(lookback), etime=now())
| summarize by DnsQuery, SrcIpAddr)
on SrcIpAddr
| summarize
StartTimeUtc = min(StartTimeUtc),
EndTimeUtc = max(EndTimeUtc),
DNSQueries=make_list(DnsQuery, 100)
by SrcIpAddr, DNSQueryCount
| extend DNSQueryThreshold=threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
DNSQueries: DNSQueries
DNSQueryThreshold: DNSQueryThreshold
DNSQueryCount: DNSQueryCount
alertDetailsOverride:
alertDisplayNameFormat: "[Static threshold] Potential DGA (Domain Generation Algorithm) originating from client IP: '{{SrcIpAddr}}' has been detected."
alertDescriptionFormat: "Client has been identified with high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). This client is found to be communicating with multiple Domains which do not exist.\n\nDGA DNS query count baseline is: '{{DNSQueryThreshold}}'\n\nCurrent failed DNS query count from this client: '{{DNSQueryCount}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
let lookback = 10d;
let referenceendtime = 1d;
let nxDomainDnsEvents is inlined into the numbered stages below.
Stages 1 to 4 define let nxDomainDnsEvents as the rule's main pipeline source. Stages 5 to 10 use it.
Stage 1: source
_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
Stage 2: where
| where DnsQueryTypeName in ("A", "AAAA")
Stage 3: where
| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
Stage 4: where
| where DnsQuery !contains "/" and DnsQuery contains "."
Stage 5: summarize
nxDomainDnsEvents
| summarize
StartTimeUtc = min(TimeGenerated),
EndTimeUtc = max(TimeGenerated),
DNSQueryCount=dcount(DnsQuery)
by SrcIpAddr
Stage 6: where
| where DNSQueryCount > threshold
Stage 7: join (negated)
join kind=leftanti (nxDomainDnsEvents) on SrcIpAddr
Stage 8: join
| join kind = inner (nxDomainDnsEvents (stime=ago(lookback), etime=now())
| summarize by DnsQuery, SrcIpAddr)
on SrcIpAddr
Stage 9: summarize
summarize DNSQueries, EndTimeUtc, StartTimeUtc by SrcIpAddr, DNSQueryCount
Stage 10: extend
extend DNSQueryThreshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DNSQueryCount | gt |
| field:"DNSQueryCount" kind:gt value:"100" |
DnsQuery | contains |
| field:"QueryName" kind:contains value:"." |
DnsQueryTypeName | in |
| field:"DnsQueryTypeName" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | cidr_match | 127.0.0.1 | excludes:SrcIpAddr field:"SrcIpAddr" value:"127.0.0.1" |
DnsQuery | contains | / | excludes:DnsQuery field:"DnsQuery" value:"/" |
DNSQueryCount | gt | 100 | excludes:DNSQueryCount field:"DNSQueryCount" value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DNSQueries | summarize |
DNSQueryCount | summarize |
EndTimeUtc | summarize |
SrcIpAddr | summarize |
StartTimeUtc | summarize |
DNSQueryThreshold | extend |
Ransom Protect User Blocked
#Detects malicious users blocked by CTERA Ransom Protect AI engine.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: d5d4766b-e547-44da-9d85-48ff393db201
name: Ransom Protect User Blocked
description: 'Detects malicious users blocked by CTERA Ransom Protect AI engine.'
status: Available
severity: High
kind: NRT
requiredDataConnectors:
- connectorId: CTERA
dataTypes:
- Syslog
tactics:
- Impact
relevantTechniques:
- T1486
query: |
Syslog
| where SyslogMessage contains "Ransom Protect mechanism blocked"
| extend
Portal = extract("portal:(\\w+)", 1, SyslogMessage),
EdgeFiler = extract("Edge Filer:(\\w+-\\d+)", 1, SyslogMessage),
IP = extract("IP:([0-9.]+)", 1, SyslogMessage),
User = extract("user:(\\w+)", 1, SyslogMessage),
BlockedTime = extract("at ([^ ]+)", 1, SyslogMessage)
| project TimeGenerated, Portal, EdgeFiler, IP, User, BlockedTime
suppressionDuration: PT5H
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertnameFormat: 'CTERA Ransom Protect User Blocked'
alertDescriptionFormat: CTERA Ransom Protect blocked a malicious user at {{TimeGenerated}}.
customDetails:
EdgeFiler: EdgeFiler
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IP
version: 1.0.2
Stages and Predicates
Stage 1: source
Syslog
Stage 2: where
| where SyslogMessage contains "Ransom Protect mechanism blocked"
Stage 3: extend
| extend
Portal = extract("portal:(\\w+)", 1, SyslogMessage),
EdgeFiler = extract("Edge Filer:(\\w+-\\d+)", 1, SyslogMessage),
IP = extract("IP:([0-9.]+)", 1, SyslogMessage),
User = extract("user:(\\w+)", 1, SyslogMessage),
BlockedTime = extract("at ([^ ]+)", 1, SyslogMessage)
Stage 4: project
| project TimeGenerated, Portal, EdgeFiler, IP, User, BlockedTime
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | contains |
| field:"SyslogMessage" kind:contains value:"Ransom Protect mechanism blocked" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
BlockedTime | project |
EdgeFiler | project |
IP | project |
Portal | project |
TimeGenerated | project |
User | project |
Tape Media Pool Deleted
#Detects when a tape media pool is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: ceb625a3-a8d7-4a42-9ea1-40f01c337ecd
name: Tape Media Pool Deleted
description: Detects when a tape media pool is deleted from Veeam Backup & Replication.
This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 23630\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 23630
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"23630" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Veeam ONE Job Disabled
#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 a Veeam ONE job is disabled.
Rule Body
id: 1a9e31d6-2116-4ff1-8ee7-abc957c34350
name: Veeam ONE Job Disabled
description: Detects when a Veeam ONE job is disabled.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: VeeamCustomTablesDataConnector
dataTypes:
- VeeamOneTriggeredAlarms_CL
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: VeeamOneTriggeredAlarms_CL | where PredefinedAlarmId == 316
version: 1.0.1
kind: Scheduled
customDetails:
PredefinedAlarmId: PredefinedAlarmId
TriggeredAlarmId: TriggeredAlarmId
Description: Description
Name: Name
VoneHostName: VoneHostName
TriggeredTime: TriggeredTime
Status: Status
Comment: Comment
ObjectId: ObjectId
ObjectName: ObjectName
ObjectType: ObjectType
Stages and Predicates
Stage 1: source
VeeamOneTriggeredAlarms_CL
Stage 2: where
| where PredefinedAlarmId == 316
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PredefinedAlarmId | eq |
| field:"PredefinedAlarmId" kind:eq value:"316" |
VMware ESXi - Low patch disk space
#'This rule is triggered when low patch disk store space is detected.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 48d992ba-d404-4159-a8c6-46f51d1325c7
name: VMware ESXi - Low patch disk space
description: |
'This rule is triggered when low patch disk store space is detected.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1529
query: |
let threshold = 100;
VMwareESXi
| where SyslogMessage has ('Patch store disk')
| extend sp = toreal(extract(@'free space is:\s(\d+)', 1, SyslogMessage)) / 1000000000
| where sp < threshold
| extend h = 'Hypervisor'
| extend HostCustomEntity = h
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
Stage 1: source
VMwareESXi
Stage 2: where
| where SyslogMessage has ('Patch store disk')
Stage 3: extend
| extend sp = toreal(extract(@'free space is:\s(\d+)', 1, SyslogMessage)) / 1000000000
Stage 4: where
| where sp < threshold
Stage 5: extend
| extend h = 'Hypervisor'
Stage 6: extend
| extend HostCustomEntity = h
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"Patch store disk" |
sp | lt |
| field:"sp" kind:lt value:"100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
sp | extend |
h | extend |
HostCustomEntity | extend |