Detection rules › Kusto
Kusto rules: 2h
[Entra ID] Domain Federation Trust Settings Modified
#Detects changes to domain federation trust settings or domain authentication mode. These changes can be used to redirect sign-in trust and should be reviewed immediately.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Set domain authentication |
| Azure | Set federation settings on domain |
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure AD New Custom Domain Added (Splunk)
- Azure AD New Federated Domain Added (Splunk)
- Azure Domain Federation Settings Modified (Sigma)
- Entra ID Custom Domain Added or Verified (Elastic)
- Entra ID Domain Federation Configuration Change (Elastic)
- Modified domain federation trust settings (Kusto)
- New onmicrosoft domain added to tenant (Kusto)
- New Root Certificate Authority Added (Sigma)
Rule Body
id: 944d0ab5-b654-47f9-a398-2e77a0b7906e
name: "[Entra ID] Domain Federation Trust Settings Modified"
version: 1.0.0
kind: Scheduled
description: |
Detects changes to domain federation trust settings or domain authentication mode. These changes can be used to redirect sign-in trust and should be reviewed immediately.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- Persistence
relevantTechniques:
- T1555
- T1098
query: |
(union isfuzzy=true
(
AuditLogs
| where OperationName =~ "Set federation settings on domain"
//| where Result =~ "success" // commenting out, as it may be interesting to capture failed attempts
| mv-expand TargetResources
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
| mv-expand modifiedProperties
| extend targetDisplayName = tostring(parse_json(modifiedProperties).displayName)
),
(
AuditLogs
| where OperationName =~ "Set domain authentication"
//| where Result =~ "success" // commenting out, as it may be interesting to capture failed attempts
| mv-expand TargetResources
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
| mv-expand modifiedProperties
| mv-apply Property = modifiedProperties on
(
where Property.displayName =~ "LiveType"
| extend
targetDisplayName = tostring(Property.displayName),
NewDomainValue = tostring(Property.newValue)
)
| where NewDomainValue has "Federated"
)
)
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
| extend InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.displayName)
| extend
InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]),
InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend Source_Network_IPLocation = ""
| project
Alert_Time_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Alert_Time_UTC0 = TimeGenerated,
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Config Modification",
Alert_Name_en = "Domain Federation Trust Settings Modified",
Alert_Description_en=strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
" in Entra ID: ",
"",
", detected that Entra ID domain authentication was set to Federated",
" operation action: ",
iff(isnotempty(OperationName), OperationName, "<NoOperationName>"),
", changed property: ",
iff(isnotempty(targetDisplayName), targetDisplayName, "<NoProperty>"),
", operator/application: ",
iff(isnotempty(InitiatingUserOrApp), InitiatingUserOrApp, "<NoInitiator>"),
", source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", location: ",
iff(isnotempty(Source_Network_IPLocation), Source_Network_IPLocation, "<NoLocation>"),
", User-Agent:",
iff(isnotempty(UserAgent), UserAgent, "<NoUserAgent>"),
"This is a high-risk domain-level change that may allow the entire tenant identity to be taken over by external federation."
),
Alert_TriageStep_en=strcat(
"1. Confirm whether the change was authorized. Operation action: ",
iff(isnotempty(OperationName), OperationName, "<NoOperationName>"),
", operator: ",
iff(isnotempty(InitiatingUserOrApp), InitiatingUserOrApp, "<NoInitiator>"),
" has formal approval.",
"2. Check source risk. Source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", location: ",
iff(isnotempty(Source_Network_IPLocation), Source_Network_IPLocation, "<NoLocation>"),
" is a company named location or an abnormal country.",
"3. Check the operation tool User-Agent: ",
iff(isnotempty(UserAgent), UserAgent, "<NoUserAgent>"),
" to determine whether it is an expected management entry point (Portal/PowerShell/Graph) or a suspicious script.",
"4. Immediately inventory the impact scope: confirm which domains were set to Federated and whether all user sign-in flows are affected."
),
Alert_Containment_en=strcat(
"1. If determined to be an unauthorized change, immediately change the affected domain authentication method back to Managed and remove any unauthorized Federation settings. ",
"2. Immediately revoke the operator's sign-in tokens and administrative permissions, and reset the password and re-register MFA if necessary. ",
"3. Block the source IP immediately or restrict management-plane access to prevent continued change attempts. ",
"4. Initiate tenant emergency response: check whether any accounts have successfully signed in through the Federated domain and performed administrative operations."
),
Alert_Remediation_en=strcat(
"1. Strengthen domain-level governance: limit the role scope that can execute Set federation / Set domain authentication, and enforce two-person review. ",
"2. Strengthen Conditional Access: allow management-plane operations only from named locations, compliant devices, and MFA, and directly block abnormal countries or new User-Agents. ",
"3. Automated detection and response: establish SOAR for related events (notification, account suspension, source IP blocking). ",
"4. Regularly audit domain and federation settings: inventory all domain authentication modes and remove unnecessary or unused Federation and trust relationships."
),
//Event_Code = AliveTime,
Event_Action = OperationName,
//Event_Description = Role ,
Event_TimeRange_Start_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Event_TimeRange_End_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Source_Identity_FullName = InitiatingUserOrApp,
Source_Identity_Type = iff(isnotempty(InitiatingUserPrincipalName), "User", "Service"),
Source_Network_IPAddress = InitiatingIpAddress,
Source_Network_IPLocation = Source_Network_IPLocation,
Source_Identity_DomainType = iff(InitiatingUserPrincipalName contains "EXT", 'External', 'Internal'),
Source_Resource_Name = UserAgent,
Target_Identity_ID = targetDisplayName,
//Target_Identity_Type = "Service",
Target_Resource_ID = "",
Target_Resource_Name = "Microsoft Entra ID",
Target_Resource_Type = "Microsoft Entra ID"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Source_Identity_FullName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Source_Network_IPAddress
Stages and Predicates
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: AuditLogs, AuditLogs
Leg 1: AuditLogs
AuditLogs
| where OperationName =~ "Set federation settings on domain"
| mv-expand TargetResources
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
| mv-expand modifiedProperties
| extend targetDisplayName = tostring(parse_json(modifiedProperties).displayName)
Leg 2: AuditLogs
AuditLogs
| where OperationName =~ "Set domain authentication"
| mv-expand TargetResources
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
| mv-expand modifiedProperties
| mv-apply Property = modifiedProperties on
(
where Property.displayName =~ "LiveType"
| extend
targetDisplayName = tostring(Property.displayName),
NewDomainValue = tostring(Property.newValue)
)
| where NewDomainValue has "Federated"
Applied to the Combined Result
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
| extend InitiatingUserOrApp = iff(isnotempty(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.displayName)
| extend
InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]),
InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend Source_Network_IPLocation = ""
| project
Alert_Time_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Alert_Time_UTC0 = TimeGenerated,
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Config Modification",
Alert_Name_en = "Domain Federation Trust Settings Modified",
Alert_Description_en=strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
" in Entra ID: ",
"",
", detected that Entra ID domain authentication was set to Federated",
" operation action: ",
iff(isnotempty(OperationName), OperationName, "<NoOperationName>"),
", changed property: ",
iff(isnotempty(targetDisplayName), targetDisplayName, "<NoProperty>"),
", operator/application: ",
iff(isnotempty(InitiatingUserOrApp), InitiatingUserOrApp, "<NoInitiator>"),
", source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", location: ",
iff(isnotempty(Source_Network_IPLocation), Source_Network_IPLocation, "<NoLocation>"),
", User-Agent:",
iff(isnotempty(UserAgent), UserAgent, "<NoUserAgent>"),
"This is a high-risk domain-level change that may allow the entire tenant identity to be taken over by external federation."
),
Alert_TriageStep_en=strcat(
"1. Confirm whether the change was authorized. Operation action: ",
iff(isnotempty(OperationName), OperationName, "<NoOperationName>"),
", operator: ",
iff(isnotempty(InitiatingUserOrApp), InitiatingUserOrApp, "<NoInitiator>"),
" has formal approval.",
"2. Check source risk. Source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", location: ",
iff(isnotempty(Source_Network_IPLocation), Source_Network_IPLocation, "<NoLocation>"),
" is a company named location or an abnormal country.",
"3. Check the operation tool User-Agent: ",
iff(isnotempty(UserAgent), UserAgent, "<NoUserAgent>"),
" to determine whether it is an expected management entry point (Portal/PowerShell/Graph) or a suspicious script.",
"4. Immediately inventory the impact scope: confirm which domains were set to Federated and whether all user sign-in flows are affected."
),
Alert_Containment_en=strcat(
"1. If determined to be an unauthorized change, immediately change the affected domain authentication method back to Managed and remove any unauthorized Federation settings. ",
"2. Immediately revoke the operator's sign-in tokens and administrative permissions, and reset the password and re-register MFA if necessary. ",
"3. Block the source IP immediately or restrict management-plane access to prevent continued change attempts. ",
"4. Initiate tenant emergency response: check whether any accounts have successfully signed in through the Federated domain and performed administrative operations."
),
Alert_Remediation_en=strcat(
"1. Strengthen domain-level governance: limit the role scope that can execute Set federation / Set domain authentication, and enforce two-person review. ",
"2. Strengthen Conditional Access: allow management-plane operations only from named locations, compliant devices, and MFA, and directly block abnormal countries or new User-Agents. ",
"3. Automated detection and response: establish SOAR for related events (notification, account suspension, source IP blocking). ",
"4. Regularly audit domain and federation settings: inventory all domain authentication modes and remove unnecessary or unused Federation and trust relationships."
),
Event_Action = OperationName,
Event_TimeRange_Start_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Event_TimeRange_End_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Source_Identity_FullName = InitiatingUserOrApp,
Source_Identity_Type = iff(isnotempty(InitiatingUserPrincipalName), "User", "Service"),
Source_Network_IPAddress = InitiatingIpAddress,
Source_Network_IPLocation = Source_Network_IPLocation,
Source_Identity_DomainType = iff(InitiatingUserPrincipalName contains "EXT", 'External', 'Internal'),
Source_Resource_Name = UserAgent,
Target_Identity_ID = targetDisplayName,
Target_Resource_ID = "",
Target_Resource_Name = "Microsoft Entra ID",
Target_Resource_Type = "Microsoft Entra ID"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalDetail.key | eq |
| field:"AdditionalDetail.key" kind:eq value:"User-Agent" |
NewDomainValue | match |
| field:"NewDomainValue" kind:match value:"Federated" |
OperationName | eq |
| field:"OperationName" kind:eq |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"LiveType" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Alert_Category_en | project |
Alert_Containment_en | project |
Alert_Description_en | project |
Alert_Name_en | project |
Alert_Remediation_en | project |
Alert_SubCategory_en | project |
Alert_Time_TW | project |
Alert_Time_UTC0 | project |
Alert_TriageStep_en | project |
Event_Action | project |
Event_TimeRange_End_TW | project |
Event_TimeRange_Start_TW | project |
Source_Identity_DomainType | project |
Source_Identity_FullName | project |
Source_Identity_Type | project |
Source_Network_IPAddress | project |
Source_Network_IPLocation | project |
Source_Resource_Name | project |
Target_Identity_ID | project |
Target_Resource_ID | project |
Target_Resource_Name | project |
Target_Resource_Type | project |
Azure DevOps PAT used with Browser
#'Personal Access Tokens (PATs) are used as an alternate password to authenticate into Azure DevOps. PATs are intended for programmatic access use in code or applications. This can be prone to attacker theft if not adequately secured. This query looks for the use of a PAT in authentication but from a User Agent indicating a browser. This should not be normal activity and could be an indicator of an attacker using a stolen PAT.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 5f0d80db-3415-4265-9d52-8466b7372e3a
name: Azure DevOps PAT used with Browser
description: |
'Personal Access Tokens (PATs) are used as an alternate password to authenticate into Azure DevOps. PATs are intended for programmatic access use in code or applications.
This can be prone to attacker theft if not adequately secured. This query looks for the use of a PAT in authentication but from a User Agent indicating a browser.
This should not be normal activity and could be an indicator of an attacker using a stolen PAT.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1528
query: |
ADOAuditLogs
| where AuthenticationMechanism startswith "PAT"
// Look for useragents that include a redenring engine
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink")
| 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
Stage 1: source
ADOAuditLogs
Stage 2: where
| where AuthenticationMechanism startswith "PAT"
Stage 3: where
| where UserAgent has_any ("Gecko", "WebKit", "Presto", "Trident", "EdgeHTML", "Blink")
Stage 4: 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" |
UserAgent | match |
| field:"aws::userAgent" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | extend |
AccountUPNSuffix | extend |
Azure DevOps Service Connection Addition/Abuse - Historic allow list
#'This detection builds an allow list of historic service connection use by Builds and Releases and compares to recent history, flagging growth of service connection use which are not manually included in the allow list and not historically included in the allow list Build/Release runs. This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Impact |
Rule Body
id: 5efb0cfd-063d-417a-803b-562eae5b0301
name: Azure DevOps Service Connection Addition/Abuse - Historic allow list
description: |
'This detection builds an allow list of historic service connection use by Builds and Releases and compares to recent history, flagging growth of service connection use which are not manually included in the allow list and not historically included in the allow list Build/Release runs.
This is to determine if someone is hijacking a build/release and adding many service connections in order to abuse or dump credentials from service connections.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 6h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- Impact
relevantTechniques:
- T1098
- T1496
query: |
let starttime = 14d;
let endtime = 6h;
// Ignore Build/Releases with less/equal this number
let ServiceConnectionThreshold = 3;
// New Connections need to exhibit execution of more "new" connections than this number.
let NewConnectionThreshold = 1;
// List of Builds/Releases to ignore in your space
let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
//"103", "Release", "ProjectA",
//"42", "Release", "ProjectB",
//"122", "Build", "ProjectB"
];
let HistoricDefs = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == "Library.ServiceConnectionExecuted"
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
| summarize HistoricCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName))
by DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN;
ADOAuditLogs
| where TimeGenerated >= ago(endtime)
| where OperationName == "Library.ServiceConnectionExecuted"
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
| parse ScopeDisplayName with OrganizationName ' (Organization)'
| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
by OrganizationName, DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN
| where CurrentCount > ServiceConnectionThreshold
| join (HistoricDefs) on ProjectId, DefId, Type, ActorUPN
| join kind=anti BypassDefIds on $left.DefId==$right.DefId and $left.Type == $right.Type and $left.ProjectName == $right.ProjectName
| extend link = iff(
Type == "Build", strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId),
strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId))
| where CurrentCount >= HistoricCount + NewConnectionThreshold
| project StartTime, OrganizationName, ProjectName, DefId, link, RecentDistinctServiceConnections = CurrentCount, HistoricDistinctServiceConnections = HistoricCount,
RecentConnections = ConnectionNames, HistoricConnections = ConnectionNames1, ActorUPN
| extend timestamp = StartTime
| 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
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 6h;
let ServiceConnectionThreshold = 3;
let NewConnectionThreshold = 1;
Let Binding: BypassDefIds
let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
];
Let Binding: HistoricDefs
let HistoricDefs = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == "Library.ServiceConnectionExecuted"
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
| summarize HistoricCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName))
by DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN;
Stage 1: source
ADOAuditLogs
Stage 2: where
| where TimeGenerated >= ago(endtime)
Stage 3: where
| where OperationName == "Library.ServiceConnectionExecuted"
Stage 4: extend
| extend DefId = tostring(Data.DefinitionId), Type = tostring(Data.PlanType), ConnectionId = tostring(Data.ConnectionId)
Stage 5: parse
| parse ScopeDisplayName with OrganizationName ' (Organization)'
Stage 6: summarize
| summarize CurrentCount = dcount(tostring(ConnectionId)), ConnectionNames = make_set(tostring(Data.ConnectionName)), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated)
by OrganizationName, DefId = tostring(DefId), Type = tostring(Type), ProjectId, ProjectName, ActorUPN
Stage 7: where
| where CurrentCount > ServiceConnectionThreshold
Stage 8: join
| join (HistoricDefs) on ProjectId, DefId, Type, ActorUPN
Stage 9: join (negated)
join kind=anti (BypassDefIds)
Stage 10: extend
extend link
link =Type == "Build"strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId)strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_release?_a=releases&view=mine&definitionId=', DefId)Stage 11: where
where CurrentCount >= (HistoricCount + 1)
Stage 12: project
project ActorUPN, DefId, HistoricConnections, HistoricDistinctServiceConnections, OrganizationName, ProjectName, RecentConnections, RecentDistinctServiceConnections, StartTime, link
Stage 13: extend
extend timestamp
Stage 14: extend
extend AccountName, AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CurrentCount | gt |
| field:"CurrentCount" kind:gt value:"3" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Library.ServiceConnectionExecuted" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUPN | project |
DefId | project |
HistoricConnections | project |
HistoricDistinctServiceConnections | project |
OrganizationName | project |
ProjectName | project |
RecentConnections | project |
RecentDistinctServiceConnections | project |
StartTime | project |
link | project |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
BloodHound Attack Path Finding - Large Default Groups in DCOM Users Groups
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Large Default Groups in DCOM Users Groups
Rule Body
id: e3c95148-04b6-4165-b9a6-a107f8a119d9
name: BloodHound Attack Path Finding - Large Default Groups in DCOM Users Groups
description: BloodHound Attack Path Finding - Large Default Groups in DCOM Users Groups
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 == "LargeDefaultGroupsDCOM"
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 == "LargeDefaultGroupsDCOM"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"LargeDefaultGroupsDCOM" |
BloodHound Attack Path Finding - WriteOwnerLimitedRights Privileges on Tier Zero Objects
#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 - WriteOwnerLimitedRights Privileges on Tier Zero Objects
Rule Body
id: 645b0f09-f318-41c3-860d-6c0fb595af2b
name: BloodHound Attack Path Finding - WriteOwnerLimitedRights Privileges on Tier
Zero Objects
description: BloodHound Attack Path Finding - WriteOwnerLimitedRights Privileges on
Tier Zero Objects
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 == "T0WriteOwnerLimitedRights"
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 == "T0WriteOwnerLimitedRights"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0WriteOwnerLimitedRights" |
BloodHound Attack Path Finding - WriteServicePrincipalName Privileges on Tier Zero Objects
#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 - WriteServicePrincipalName Privileges on Tier Zero Objects
Rule Body
id: b58af025-1b74-436a-8517-b230864cbbb6
name: BloodHound Attack Path Finding - WriteServicePrincipalName Privileges on Tier
Zero Objects
description: BloodHound Attack Path Finding - WriteServicePrincipalName Privileges
on Tier Zero Objects
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 == "T0WriteSPN"
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 == "T0WriteSPN"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0WriteSPN" |
Box - Inactive user login
#'Detects user login after long inactivity period.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: edbf38d7-e170-4af2-ad50-1a05b374611b
name: Box - Inactive user login
description: |
'Detects user login after long inactivity period.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoxDataConnector
dataTypes:
- BoxEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let lbperiod_start = 14d;
let lbperiod_end = 7d;
let lbtime = 1h;
let active_users = BoxEvents
| where EventEndTime between (ago(lbperiod_end) .. ago(lbtime))
| where EventType =~ 'LOGIN'
| summarize makeset(SourceName);
let inactive_users = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| where EventType =~ 'LOGIN'
| where SourceName !in (active_users)
| summarize makeset(SourceName);
BoxEvents
| where EventType == 'LOGIN'
| where SourceName in (inactive_users)
| extend AccountCustomEntity = SourceName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod_start = 14d;
let lbperiod_end = 7d;
let lbtime = 1h;
Let Binding: active_users
let active_users = BoxEvents
| where EventEndTime between (ago(lbperiod_end) .. ago(lbtime))
| where EventType =~ 'LOGIN'
| summarize makeset(SourceName);
Let Binding: inactive_users
let inactive_users = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| where EventType =~ 'LOGIN'
| where SourceName !in (active_users)
| summarize makeset(SourceName);
Stage 1: source
BoxEvents
Stage 2: where
| where EventType == 'LOGIN'
Stage 3: where
| where SourceName in (inactive_users)
Stage 4: extend
| extend AccountCustomEntity = SourceName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"LOGIN" |
SourceName | in |
| field:"SourceName" kind:in value:"inactive_users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Certified Pre-Owned - backup of CA private key - rule 1
#This query identifies someone that performs a read operation of they CA key from the file.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5058: Key file operation. |
Rule Body
id: aa5eaac7-1264-4833-b620-8f062be75541
name: Certified Pre-Owned - backup of CA private key - rule 1
description: |
This query identifies someone that performs a read operation of they CA key from the file.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1036
query: |
SecurityEvent
// Fill in the machine name of your CA.
| where EventID == 5058 and Computer contains "<YOUR CA MACHINE NAME>"
| where EventData contains "%%2499" // Machine key.
| extend EventData=parse_xml(EventData)
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="KeyName"
| project KeyName=tostring(d["#text"])
)
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="SubjectUserName"
| project SubjectUserName=tostring(d["#text"])
)
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="Operation"
| project Operation=tostring(d["#text"])
)
| extend Operation=iff(Operation == "%%2458", "Read persisted key from file", Operation)
// This one is a guess and very poorly documented :(
| extend Operation=iff(Operation == "%%2459", "Write persisted key to file", Operation)
// Fill in the keyname of the CA key.
| where KeyName == "<INSERT ISSUING CA KEY HERE>" // Or any other key you want to monitor.
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5058 and Computer contains "<YOUR CA MACHINE NAME>"
Stage 3: where
| where EventData contains "%%2499"
Stage 4: extend
| extend EventData=parse_xml(EventData)
Stage 5: kusto:mv-apply
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="KeyName"
| project KeyName=tostring(d["#text"])
)
Stage 6: kusto:mv-apply
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="SubjectUserName"
| project SubjectUserName=tostring(d["#text"])
)
Stage 7: kusto:mv-apply
| mv-apply d=EventData.EventData.Data on
(
where d["@Name"]=="Operation"
| project Operation=tostring(d["#text"])
)
Stage 8: extend
| extend Operation=iff(Operation == "%%2458", "Read persisted key from file", Operation)
Operation =Operation == "%%2458""Read persisted key from file"OperationStage 9: extend
| extend Operation=iff(Operation == "%%2459", "Write persisted key to file", Operation)
Operation =Operation == "%%2459""Write persisted key to file"OperationStage 10: where
| where KeyName == "<INSERT ISSUING CA KEY HERE>"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Computer | contains |
| field:"Computer" kind:contains value:"<YOUR CA MACHINE NAME>" |
EventData | contains |
| field:"EventData" kind:contains value:"%%2499" |
EventID | eq |
| field:"EventID" kind:eq value:"5058" |
KeyName | eq |
| field:"KeyName" kind:eq value:"<INSERT ISSUING CA KEY HERE>" |
d["@Name"] | eq |
|
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventData | extend |
Operation | extend |
Cloudflare - Empty user agent
#'Detects requests where user agent is empty.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 729c6d21-fad9-4a6a-9c7f-482393c95958
name: Cloudflare - Empty user agent
description: |
'Detects requests where user agent is empty.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
Cloudflare
| where isempty(HttpUserAgentOriginal)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
Cloudflare
Stage 2: where
| where isempty(HttpUserAgentOriginal)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpUserAgentOriginal | is_null | field:"HttpUserAgentOriginal" kind:is_null |
Corelight - External Proxy Detected
#'Detects external proxy usage.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 05850746-9ae4-412f-838b-844f0903f4a9
name: Corelight - External Proxy Detected
description: |
'Detects external proxy usage.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Corelight
dataTypes:
- corelight_http
- corelight_http_red
- corelight_http2
- corelight_http_agg
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- CommandAndControl
relevantTechniques:
- T1090
query: |
union isfuzzy=true corelight_http, corelight_http_agg
| where isnotempty(proxied)
| where not(ipv4_is_private(id_orig_h))
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: id_orig_h
version: 2.2.0
kind: Scheduled
Stages and Predicates
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: corelight_http, corelight_http_agg
Leg 1: corelight_http
Leg 2: corelight_http_agg
Applied to the Combined Result
| where isnotempty(proxied) | where not(ipv4_is_private(id_orig_h))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
proxied | is_not_null | field:"proxied" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
id_orig_h | 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:id_orig_h |
Dataverse - Terminated employee exfiltration to USB drive
#Identifies files downloaded from Dataverse by departing or terminated employees which are copied to USB mounted drives.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry Coverage
Rule Body
id: c5e75cb6-cea0-49c2-a998-da414035aac1
kind: Scheduled
name: Dataverse - Terminated employee exfiltration to USB drive
description: Identifies files downloaded from Dataverse by departing or terminated
employees which are copied to USB mounted drives.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceInfo
- DeviceEvents
- DeviceFileEvents
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1052
query: |
let drive_mount_lookback = 14d;
let query_frequency = 1h;
DataverseActivity
| distinct InstanceUrl
| join kind=inner (DeviceFileEvents
| where TimeGenerated >= ago(query_frequency))
on $left.InstanceUrl == $right.FileOriginUrl
| join kind=inner (MSBizAppsTerminatedEmployees()) on $left.InitiatingProcessAccountUpn == $right.UserPrincipalName
| join kind=inner (DeviceEvents
| where TimeGenerated >= ago(drive_mount_lookback)
| where ActionType == "UsbDriveMounted"
| extend DriveLetter = tostring(AdditionalFields.DriveLetter)
| summarize MountedDriveLetters = make_set(DriveLetter, 26) by DeviceId, DeviceName)
on DeviceId
| extend TargetDriveLetter = tostring(split(FolderPath, "\\")[0])
| where set_has_element(MountedDriveLetters, TargetDriveLetter)
| join kind=inner (DeviceInfo
| summarize arg_max(TimeGenerated, DeviceId, PublicIP) by DeviceName)
on DeviceId
| project-rename
UserId = UserPrincipalName
| summarize LatestEvent = arg_max(TimeGenerated, *), Files = make_set(FileName, 100) by UserId, InstanceUrl
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
LatestEvent,
UserId,
PublicIP,
Files,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: PublicIP
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: Dataverse - terminated user copied files from {{InstanceUrl}}
to USB
alertDescriptionFormat: '{{UserId}} , on the TerminatedUsers watchlist, was found
to copy files to a USB mounted drive.'
customDetails: {}
version: 3.2.0
Stages and Predicates
Parameters
let drive_mount_lookback = 14d;
let query_frequency = 1h;
Stage 1: source
DataverseActivity
Stage 2: distinct
| distinct InstanceUrl
Stage 3: join
| join kind=inner (DeviceFileEvents
| where TimeGenerated >= ago(query_frequency))
on $left.InstanceUrl == $right.FileOriginUrl
Stage 4: join
| join kind=inner (MSBizAppsTerminatedEmployees()) on $left.InitiatingProcessAccountUpn == $right.UserPrincipalName
Stage 5: join
| join kind=inner (DeviceEvents
| where TimeGenerated >= ago(drive_mount_lookback)
| where ActionType == "UsbDriveMounted"
| extend DriveLetter = tostring(AdditionalFields.DriveLetter)
| summarize MountedDriveLetters = make_set(DriveLetter, 26) by DeviceId, DeviceName)
on DeviceId
Stage 6: extend
extend TargetDriveLetter
Stage 7: where
where TargetDriveLetter =~ "MountedDriveLetters"
Stage 8: join
| join kind=inner (DeviceInfo
| summarize arg_max(TimeGenerated, DeviceId, PublicIP) by DeviceName)
on DeviceId
Stage 9: project-rename
project-rename
Stage 10: summarize
summarize Files, LatestEvent by UserId, InstanceUrl
Stage 11: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 12: project
project AccountName, CloudAppId, Files, InstanceUrl, LatestEvent, PublicIP, UPNSuffix, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"UsbDriveMounted" |
TargetDriveLetter | in |
| field:"TargetDriveLetter" kind:in value:"MountedDriveLetters" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
CloudAppId | project |
Files | project |
InstanceUrl | project |
LatestEvent | project |
PublicIP | project |
UPNSuffix | project |
UserId | project |
Defender Alert Evidence
#This analytic rule is looking for new alert evidence from Microsoft Defender for Endpoint. The intent is to create entries in the SecurityAlert table for every new alert evidence attached to an entity of type Device or User monitored by Defender for Endpoint.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | AlertEvidence action any: Alert evidence |
Rule Body
id: 8138863e-e55f-4f02-ac94-72796e203d27
name: Defender Alert Evidence
version: 1.0.0
type: Microsoft.OperationalInsights/workspaces/providers/alertRules
kind: Scheduled
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- AlertEvidence
severity: High
description: This analytic rule is looking for new alert evidence from Microsoft Defender for Endpoint. The intent is to create entries in the SecurityAlert table for every new alert evidence attached to an entity of type Device or User monitored by Defender for Endpoint.
tactics:
- Persistence
relevantTechniques:
- T1546
query: |
AlertEvidence
| where EntityType in ("Device", "User")
queryFrequency: 60m
queryPeriod: 60m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: PT5H
suppressionEnabled: false
incidentConfiguration:
createIncident: false
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: LocalIP
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
Stages and Predicates
Stage 1: source
AlertEvidence
Stage 2: where
| where EntityType in ("Device", "User")
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EntityType | in |
| field:"EntityType" kind:in |
Detect Custom Script or Run Command deployment by risky user
#This detection rule flags when a user with risk events in Entra ID Identity Protection is deploying Custom Scripts or Run Commands on Azure or Azure Arc machines. This may indicate a compromised cloud user that is now performaring lateral movement from the Azure control plane to Virtual Machines in other environments.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Lateral Movement |
References
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure Active Directory Hybrid Health AD FS New Server (Sigma)
- Azure Active Directory Hybrid Health AD FS Service Delete (Sigma)
- Azure Subscription Permission Elevation Via AuditLogs (Sigma)
- Microsoft Entra ID Hybrid Health AD FS New Server (Kusto)
- Microsoft Entra ID Hybrid Health AD FS Service Delete (Kusto)
- Microsoft Entra ID Hybrid Health AD FS Suspicious Application (Kusto)
- NRT Microsoft Entra ID Hybrid Health AD FS New Server (Kusto)
Rule Body
AzureActivity
| where TimeGenerated > ago(1h)
| where CategoryValue == "Administrative"
| where OperationNameValue =~ "Microsoft.Compute/virtualMachines/runCommand/action"
or OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE"
| extend VMName = tostring(todynamic(Properties).resource)
| summarize make_list(ActivityStatusValue), TimeGenerated = max(TimeGenerated) by CorrelationId, CallerIpAddress, Caller, ResourceGroup, VMName
| join kind=inner (AADUserRiskEvents | where TimeGenerated > ago(14d) ) on $left.Caller == $right.UserPrincipalName
Stages and Predicates
Stage 1: source
AzureActivity
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where CategoryValue == "Administrative"
Stage 4: where
| where OperationNameValue =~ "Microsoft.Compute/virtualMachines/runCommand/action"
or OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE"
Stage 5: extend
| extend VMName = tostring(todynamic(Properties).resource)
Stage 6: summarize
| summarize make_list(ActivityStatusValue), TimeGenerated = max(TimeGenerated) by CorrelationId, CallerIpAddress, Caller, ResourceGroup, VMName
Stage 7: join
| join kind=inner (AADUserRiskEvents | where TimeGenerated > ago(14d) ) on $left.Caller == $right.UserPrincipalName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CategoryValue | eq |
| field:"CategoryValue" kind:eq value:"Administrative" |
OperationNameValue | eq |
| field:"azure_ad::operation_name_value" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Caller | summarize |
CallerIpAddress | summarize |
CorrelationId | summarize |
ResourceGroup | summarize |
TimeGenerated | summarize |
VMName | summarize |
GCP IAM - New Authentication Token for Service Account
#Detects when new authentication token is created for service account.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GCP | GenerateAccessToken |
Rule Body
id: 80e4db30-5636-4fbd-8816-24c3ded8d243
name: GCP IAM - New Authentication Token for Service Account
description: |
'Detects when new authentication token is created for service account.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GCPIAMDataConnector
dataTypes:
- GCP_IAM
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1550
query: |
GCP_IAM
| where PayloadMethodname in ('GenerateAccessToken', 'google.iam.admin.v1.GenerateAccessToken')
| extend result = parse_json(todynamic(PayloadAuthorizationinfo))[0]['granted']
| where result =~ 'true'
| extend service_account = extract(@'serviceAccounts\/(.*?)@', 1, PayloadResponseName)
| project-away result
| extend AccountName = tostring(split(service_account, "@")[0]), AccountUPNSuffix = tostring(split(service_account, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: service_account
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
GCP_IAM
Stage 2: where
| where PayloadMethodname in ('GenerateAccessToken', 'google.iam.admin.v1.GenerateAccessToken')
Stage 3: extend
| extend result = parse_json(todynamic(PayloadAuthorizationinfo))[0]['granted']
Stage 4: where
| where result =~ 'true'
Stage 5: extend
| extend service_account = extract(@'serviceAccounts\/(.*?)@', 1, PayloadResponseName)
Stage 6: project-away
| project-away result
Stage 7: extend
| extend AccountName = tostring(split(service_account, "@")[0]), AccountUPNSuffix = tostring(split(service_account, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PayloadMethodname | in |
| field:"gcp::method_name" kind:in |
result | eq |
| field:"result" kind:eq value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
service_account | extend |
AccountName | extend |
AccountUPNSuffix | extend |
GCP Security Command Center - Detect DNSSEC disabled for DNS zones
#Detects Google Cloud DNS zones where DNSSEC is disabled using Security Command Center findings (DNSSEC_DISABLED). Disabling DNSSEC increases risk of DNS hijacking and man-in-the-middle attacks. This analytic rule alerts on findings where DNSSEC is reported as disabled for a managed zone.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Collection | |
| Command & Control |
Rule Body
id: a9c7a4be-b7e7-4045-8028-0d1ffaa049af
name: GCP Security Command Center - Detect DNSSEC disabled for DNS zones
description: |
Detects Google Cloud DNS zones where DNSSEC is disabled using Security Command Center findings (DNSSEC_DISABLED).
Disabling DNSSEC increases risk of DNS hijacking and man-in-the-middle attacks. This analytic rule alerts on findings where DNSSEC is reported as disabled for a managed zone.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleSCCDefinition
dataTypes:
- GoogleCloudSCC
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- CommandAndControl
- DefenseEvasion
relevantTechniques:
- T1557
- T1071.004
- T1562.001
tags:
- CIS GCP Foundation 3.0 3.3
- NIST 800-53 R5 AC-18, CM-2, CM-6, CM-7, CM-9
- PCI-DSS v4.0 1.1.1, 1.2.1, 1.2.6, 1.2.7, 1.4.2, 1.5.1, 2.1.1, 2.2.1
- ISO-27001 v2022 A.8.9
- Cloud Controls Matrix 4 IVS-04
- NIST Cybersecurity Framework 1.0 PR-IP-1
- SOC2 v2017 CC5.2.2
- CIS Controls 8.0 4.2
query: |
GoogleCloudSCC
| where tostring(Findings.state) == "ACTIVE"
| extend FindingsJson = parse_json(Findings)
| extend FindingCategory = tostring(FindingsJson.category)
| where FindingCategory == "DNSSEC_DISABLED"
| extend ResourceName = tostring(FindingsJson.resourceName)
| extend
ResourceType = tostring(FindingsResource.type),
ExternalUri = tostring(FindingsJson.externalUri),
SourceProps = parse_json(tostring(FindingsJson.sourceProperties)),
GcpResource = parse_json(tostring(FindingsJson.resource))
// Extract project and managed zone from resourceName like //dns.googleapis.com/projects/PROJECT/managedZones/ZONE
| extend ProjectName = extract(@"projects/([^/]+)", 1, ResourceName)
| extend ManagedZone = extract(@"managedZones/([^/]+)", 1, ResourceName)
| extend Severity = tostring(FindingsJson.severity)
| summarize
TimeGenerated = max(TimeGenerated)
by ProjectName, ManagedZone, ResourceType, ResourceName, Severity, ExternalUri
entityMappings:
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ResourceName
customDetails:
ProjectName: ProjectName
ManagedZone: ManagedZone
ExternalUri: ExternalUri
alertDetailsOverride:
alertDisplayNameFormat: "GCP DNS zone {{ManagedZone}} has DNSSEC disabled"
alertDescriptionFormat: |-
DNSSEC is disabled for managed zone {{ManagedZone}} in project {{ProjectName}}. Review the zone configuration and enable DNSSEC where appropriate.
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GoogleCloudSCC
Stage 2: where
| where tostring(Findings.state) == "ACTIVE"
Stage 3: extend
| extend FindingsJson = parse_json(Findings)
Stage 4: extend
| extend FindingCategory = tostring(FindingsJson.category)
Stage 5: where
| where FindingCategory == "DNSSEC_DISABLED"
Stage 6: extend (5 consecutive steps)
| extend ResourceName = tostring(FindingsJson.resourceName)
| extend
ResourceType = tostring(FindingsResource.type),
ExternalUri = tostring(FindingsJson.externalUri),
SourceProps = parse_json(tostring(FindingsJson.sourceProperties)),
GcpResource = parse_json(tostring(FindingsJson.resource))
| extend ProjectName = extract(@"projects/([^/]+)", 1, ResourceName)
| extend ManagedZone = extract(@"managedZones/([^/]+)", 1, ResourceName)
| extend Severity = tostring(FindingsJson.severity)
Stage 7: summarize
| summarize
TimeGenerated = max(TimeGenerated)
by ProjectName, ManagedZone, ResourceType, ResourceName, Severity, ExternalUri
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
FindingCategory | eq |
| field:"FindingCategory" kind:eq value:"DNSSEC_DISABLED" |
Findings.state | eq |
| field:"Findings.state" kind:eq value:"ACTIVE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ExternalUri | summarize |
ManagedZone | summarize |
ProjectName | summarize |
ResourceName | summarize |
ResourceType | summarize |
Severity | summarize |
TimeGenerated | summarize |
Illumio Enforcement Change Analytic Rule
#'Create Microsoft Sentinel Incident When Ven Changes Enforcement State from Full/Selective To Idle/Visibility state'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 599fdc92-eb6d-4b54-8d79-2a3f740a846a
name: Illumio Enforcement Change Analytic Rule
description: |
'Create Microsoft Sentinel Incident When Ven Changes Enforcement State from Full/Selective To Idle/Visibility state'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: IllumioSaaSDataConnector
dataTypes:
- Illumio_Auditable_Events_CL
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 60m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
let enf_state = dynamic(["full", "selective"]);
let visibility_state = dynamic(["visibility_only", "idle"]);
Illumio_Auditable_Events_CL
| union IllumioSyslogAuditEvents
| extend temp_resource_changes = parse_json(resource_changes)[0]
| where event_type == 'workloads.update'
| extend old_mode = temp_resource_changes.changes.enforcement_mode.before,
new_mode = temp_resource_changes.changes.enforcement_mode.after,
workload_href = temp_resource_changes.resource.workload.href,
workload_name = temp_resource_changes.resource.workload.hostname,
ipaddress = action.src_ip
| where new_mode in (visibility_state) and old_mode in (enf_state)
| project-away temp_*
| project old_mode, new_mode, workload_href, workload_name, TimeGenerated, created_by, ipaddress
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: workload_name
- entityType: Account
fieldMappings:
- identifier: Name
columnName: created_by
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipaddress
alertDetailsOverride:
alertDisplayNameFormat: |
Illumio Enforcement Change Incident for {{workload_name}}
alertDescriptionFormat: |
Illumio Enforcement Change Incident for {{workload_name}} generated at {{TimeGenerated}}
version: 1.0.7
kind: Scheduled
Stages and Predicates
Parameters
let enf_state = dynamic(["full", "selective"]);
let visibility_state = dynamic(["visibility_only", "idle"]);
Stage 1: source
Illumio_Auditable_Events_CL
Stage 2: union
union of 1 branches
Stage 3: source
IllumioSyslogAuditEvents
Stage 4: extend
extend temp_resource_changes
Stage 5: where
where event_type == "workloads.update"
Stage 6: extend
extend ipaddress, new_mode, old_mode, workload_href, workload_name
Stage 7: where
where new_mode in ("idle", "visibility_only") and old_mode in ("full", "selective")
Stage 8: project-away
project-away temp_*
Stage 9: project
project TimeGenerated, created_by, ipaddress, new_mode, old_mode, workload_href, workload_name
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
event_type | eq |
| field:"event_type" kind:eq value:"workloads.update" |
new_mode | in |
| field:"new_mode" kind:in |
old_mode | in |
| field:"old_mode" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
TimeGenerated | project |
created_by | project |
ipaddress | project |
new_mode | project |
old_mode | project |
workload_href | project |
workload_name | project |
MFA Rejected by User
#Identifies occurances where a user has rejected an MFA prompt. This could be an indicator that a threat actor has compromised the username and password of this user account and is using it to try and log into the account. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results. Please note, MFA Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges or location filter after careful consideration
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: d99cf5c3-d660-436c-895b-8a8f8448da23
name: MFA Rejected by User
description: |
'Identifies occurances where a user has rejected an MFA prompt. This could be an indicator that a threat actor has compromised the username and password of this user account and is using it to try and log into the account.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
This query has also been updated to include UEBA logs IdentityInfo and BehaviorAnalytics for contextual information around the results.
Please note, MFA Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges or location filter after careful consideration'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: BehaviorAnalytics
dataTypes:
- BehaviorAnalytics
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let riskScoreCutoff = 3; //Adjust this score threshold based on volume of results. Activities identified as the most abnormal receive the highest scores (on a scale of 0-10)
SigninLogs
| where ResultType == 500121
| extend additionalDetails_ = tostring(Status.additionalDetails)
| extend UserPrincipalName = tolower(UserPrincipalName)
| where additionalDetails_ =~ "MFA denied; user declined the authentication" or additionalDetails_ has "fraud"
| summarize StartTime = min(TimeGenerated), EndTIme = max(TimeGenerated) by UserPrincipalName, UserId, AADTenantId, FailedIPAddress = IPAddress
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
| join kind=leftouter (
IdentityInfo
| summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
| project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
| summarize
Tags = make_set(Tags, 1000),
GroupMembership = make_set(GroupMembership, 1000),
AssignedRoles = make_set(AssignedRoles, 1000),
UserType = make_set(UserType, 1000),
UserAccountControl = make_set(UserType, 1000)
by AccountUPN
| extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName
//Below it will be joined with BehaviorAnalytics table to the Failed IP Addresses
| join kind=leftouter (
BehaviorAnalytics
| where ActivityType in ("FailedLogOn", "LogOn")
| where isnotempty(SourceIPAddress)
| project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
| project-rename FailedIPAddress = SourceIPAddress, Name = UserName
| summarize
MaxInvestigationScore = max(InvestigationPriority) // Only retrieve maximum Investigation Property score for both FailedIP and User
by FailedIPAddress, Name)
on FailedIPAddress, Name // Joining on both IP and User so as to only return context associated with same user
| extend UEBARiskScore = MaxInvestigationScore
| project-away *1 // removing duplicate columns post outer join from output
| where UEBARiskScore > riskScoreCutoff
| sort by UEBARiskScore desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: UserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: FailedIPAddress
version: 2.0.4
kind: Scheduled
Stages and Predicates
Parameters
let riskScoreCutoff = 3;
Stage 1: source
SigninLogs
Stage 2: where
| where ResultType == 500121
Stage 3: extend
| extend additionalDetails_ = tostring(Status.additionalDetails)
Stage 4: extend
| extend UserPrincipalName = tolower(UserPrincipalName)
Stage 5: where
| where additionalDetails_ =~ "MFA denied; user declined the authentication" or additionalDetails_ has "fraud"
Stage 6: summarize
| summarize StartTime = min(TimeGenerated), EndTIme = max(TimeGenerated) by UserPrincipalName, UserId, AADTenantId, FailedIPAddress = IPAddress
Stage 7: extend
| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
Stage 8: join
| join kind=leftouter (
IdentityInfo
| summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
| project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
| summarize
Tags = make_set(Tags, 1000),
GroupMembership = make_set(GroupMembership, 1000),
AssignedRoles = make_set(AssignedRoles, 1000),
UserType = make_set(UserType, 1000),
UserAccountControl = make_set(UserType, 1000)
by AccountUPN
| extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName
Stage 9: join
| join kind=leftouter (
BehaviorAnalytics
| where ActivityType in ("FailedLogOn", "LogOn")
| where isnotempty(SourceIPAddress)
| project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
| project-rename FailedIPAddress = SourceIPAddress, Name = UserName
| summarize
MaxInvestigationScore = max(InvestigationPriority)
by FailedIPAddress, Name)
on FailedIPAddress, Name
Stage 10: extend
extend UEBARiskScore
Stage 11: project-away
project-away *1
Stage 12: where
where UEBARiskScore > 3
Stage 13: sort
sort by UEBARiskScore
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityType | in |
| field:"ActivityType" kind:in |
ResultType | eq |
| field:"ResultType" kind:eq value:"500121" |
SourceIPAddress | is_not_null | field:"SourceIPAddress" kind:is_not_null | |
UEBARiskScore | gt |
| field:"UEBARiskScore" kind:gt value:"3" |
additionalDetails_ | eq |
| field:"additionalDetails_" kind:eq value:"MFA denied; user declined the authentication" |
additionalDetails_ | match |
| field:"additionalDetails_" kind:match value:"fraud" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AADTenantId | summarize |
EndTIme | summarize |
FailedIPAddress | summarize |
StartTime | summarize |
UserId | summarize |
UserPrincipalName | summarize |
Name | extend |
UPNSuffix | extend |
UEBARiskScore | extend |
NRT Azure DevOps Audit Stream Disabled
#'Azure DevOps allow for audit logs to be streamed to external storage solutions such as SIEM solutions. An attacker looking to hide malicious Azure DevOps activity from defenders may look to disable data streams before conducting activity and then re-enabling the stream after (so as not to raise data threshold-based alarms). Looking for disabled audit streams can identify this activity, and due to the nature of the action its unlikely to have a high false positive rate.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 74ed028d-e392-40b7-baef-e69627bf89d1
name: NRT Azure DevOps Audit Stream Disabled
description: |
'Azure DevOps allow for audit logs to be streamed to external storage solutions such as SIEM solutions. An attacker looking to hide malicious Azure DevOps activity from defenders may look to disable data streams before conducting activity and then re-enabling the stream after (so as not to raise data threshold-based alarms). Looking for disabled audit streams can identify this activity, and due to the nature of the action its unlikely to have a high false positive rate.'
severity: High
status: Available
requiredDataConnectors: []
tactics:
- DefenseEvasion
relevantTechniques:
- T1562.008
query: |
ADOAuditLogs
| where OperationName =~ "AuditLog.StreamDisabledByUser"
| extend StreamType = tostring(Data.ConsumerType)
| project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, StreamType
| 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.4
kind: NRT
Stages and Predicates
Stage 1: source
ADOAuditLogs
Stage 2: where
| where OperationName =~ "AuditLog.StreamDisabledByUser"
Stage 3: extend
| extend StreamType = tostring(Data.ConsumerType)
Stage 4: project-reorder
| project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, StreamType
Stage 5: extend
| extend timestamp = TimeGenerated
Stage 6: 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 |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"AuditLog.StreamDisabledByUser" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
StreamType | extend |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
OLE object manipulation attempts stateful anomaly on database
#'This query detects batches of distinct SQL queries that execute (or attempt to) commands that could indicate potential security issues - such as attempts to manipulate OLE objects (e.g. for running malicious commands).'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rules Detecting the Same Action
These rules filter on the same operation.
- Affected rows stateful anomaly on database (Kusto)
- Credential errors 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)
- Outgoing connection attempts stateful anomaly on database (Kusto)
- Response rows stateful anomaly on database (Kusto)
Rule Body
id: dabd7284-004b-4237-b5ee-a22acab19eb2
name: OLE object manipulation attempts stateful anomaly on database
description: |
'This query detects batches of distinct SQL queries that execute (or attempt to) commands that could indicate potential security issues - such as attempts to manipulate OLE objects (e.g. for running malicious commands).'
severity: Medium
requiredDataConnectors:
- connectorId: AzureSql
dataTypes:
- AzureDiagnostics
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1190
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 hotwords = pack_array('sp_oamethod', 'sp_oacreate', 'sp_oasetproperty'); // List of monitored hot words.
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)
| extend hasHotword = iff(Statement has_any (hotwords), 1, 0)
| summarize countEvents = count(), countStatements = dcount(Statement)
, countStatementsWithHotwords = dcountif(Statement, hasHotword == 1)
, countFailedStatementsWithHotwords = dcountif(Statement, (hasHotword == 1) and (Error > 0))
, countSuccessfulStatementsWithHotwords = dcountif(Statement, ((hasHotword == 1)) and (Error == 0))
, anyMonitoredStatement = anyif(Statement, (hasHotword == 1))
, anySuccessfulMonitoredStatement = anyif(Statement, (hasHotword == 1) and (Error == 0))
, anyInfo = anyif(AdditionalInfo, hasHotword == 1)
, hotWord = anyif(extract(strcat_array(hotwords, '|'), 0, tolower(Statement)), hasHotword == 1)
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 countSlicesWithHotwords = dcountif(timeSlice, countStatementsWithHotwords >= monitoredStatementsThreshold)
by Database;
processedData
| where WindowType == 'detection'
| join kind = inner (trainingSet) on Database
| extend IsHotwordAnomalyOnStatement = iff(((countStatementsWithHotwords >= monitoredStatementsThreshold) and (countSlicesWithHotwords <= trainingSlicesThreshold)), true, false)
, anomalyScore = round(countStatementsWithHotwords/monitoredStatementsThreshold, 0)
| where IsHotwordAnomalyOnStatement == 'true'
| project TimeGenerated = timeSlice, Database, ClientIp, ApplicationName, PrincipalName, HostName, ResourceId, countEvents, countStatements, countStatementsWithHotwords, anyMonitoredStatement, anyInfo, anomalyScore, hotWord
| 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
customDetails:
HotWords: hotWord
alertDetailsOverride:
alertDisplayNameFormat: 'OLE object manipulation attempts stateful anomaly on database {{Database}}'
alertDescriptionFormat: 'An anomaly was detected on database {{Database}} with {{countStatementsWithHotwords}} statements with monitored hot words (sp_oamethod, sp_oacreate, sp_oasetproperty etc.) in the last hour. Investigate the database activity for potential malicious attempts to manipulate OLE objects for running malicious commands or other unauthorized actions.'
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 hotwords = pack_array('sp_oamethod', 'sp_oacreate', 'sp_oasetproperty');
let processedData is inlined into the numbered stages below.
Let Binding: trainingSet
let trainingSet = processedData
| where WindowType == 'training'
| summarize countSlicesWithHotwords = dcountif(timeSlice, countStatementsWithHotwords >= monitoredStatementsThreshold)
by Database;
Stages 1 to 8 define let processedData as the rule's main pipeline source. Stages 9 to 14 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: extend
| extend hasHotword = iff(Statement has_any (hotwords), 1, 0)
hasHotword =Statement has "hotwords"10Stage 6: summarize
| summarize countEvents = count(), countStatements = dcount(Statement)
, countStatementsWithHotwords = dcountif(Statement, hasHotword == 1)
, countFailedStatementsWithHotwords = dcountif(Statement, (hasHotword == 1) and (Error > 0))
, countSuccessfulStatementsWithHotwords = dcountif(Statement, ((hasHotword == 1)) and (Error == 0))
, anyMonitoredStatement = anyif(Statement, (hasHotword == 1))
, anySuccessfulMonitoredStatement = anyif(Statement, (hasHotword == 1) and (Error == 0))
, anyInfo = anyif(AdditionalInfo, hasHotword == 1)
, hotWord = anyif(extract(strcat_array(hotwords, '|'), 0, tolower(Statement)), hasHotword == 1)
by Database, ClientIp, ApplicationName, PrincipalName, timeSlice,HostName,ResourceId
Stage 7: 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 8: where
| where WindowType in ('detection', 'training')
Stage 9: where
processedData
| where WindowType == 'detection'
Stage 10: join
| join kind = inner (trainingSet) on Database
Stage 11: extend
extend IsHotwordAnomalyOnStatement, anomalyScore
IsHotwordAnomalyOnStatement =countStatementsWithHotwords >= monitoredStatementsThreshold and countSlicesWithHotwords <= trainingSlicesThresholdtruefalseStage 12: where
where IsHotwordAnomalyOnStatement == true
Stage 13: project
project ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, TimeGenerated, anomalyScore, anyInfo, anyMonitoredStatement, countEvents, countStatements, countStatementsWithHotwords, hotWord
Stage 14: 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" |
IsHotwordAnomalyOnStatement | eq |
| field:"IsHotwordAnomalyOnStatement" 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 |
countStatementsWithHotwords | project |
hotWord | project |
Name | extend |
UPNSuffix | extend |
Oracle suspicious command execution
#The query searches process creation events that are indicative of an attacker spawning OS commands from an Oracle database.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Lateral Movement |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: e6c5ff42-0f42-4cec-994a-dabb92fe36e1
name: Oracle suspicious command execution
description: |
The query searches process creation events that are indicative of an attacker spawning OS commands from an Oracle database.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
- PrivilegeEscalation
relevantTechniques:
- T1210
- T1611
query: |
let timeframe= 1h;
DeviceProcessEvents
| where Timestamp >= ago(timeframe)
| where InitiatingProcessFileName =~ "oracle.exe"
| where not(FileName in~ ("conhost.exe", "oradim.exe"))
// Begin allow-list.
// End allow-list.
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
Parameters
let timeframe = 1h;
Stage 1: source
DeviceProcessEvents
Stage 2: where
| where Timestamp >= ago(timeframe)
Stage 3: where
| where InitiatingProcessFileName =~ "oracle.exe"
Stage 4: where
| where not(FileName in~ ("conhost.exe", "oradim.exe"))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
InitiatingProcessFileName | eq |
| field:"parent_process_basename" kind:eq value:"oracle.exe" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
FileName | in | conhost.exe, oradim.exe | excludes:FileName field:"FileName" value:"conhost.exe" field:"FileName" value:"oradim.exe" |
Password spray attack against Microsoft Entra ID Seamless SSO
#This query detects when there is a spike in Microsoft Entra ID Seamless SSO errors. They may not be caused by a Password Spray attack, but the cause of the errors might need to be investigated. Microsoft Entra ID only logs the requests that matched existing accounts, thus there might have been unlogged requests for non-existing accounts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Sign-in record (AADNonInteractiveUserSignInLogs, any result) |
Rule Body
id: fb7ca1c9-e14c-40a3-856e-28f3c14ea1ba
name: Password spray attack against Microsoft Entra ID Seamless SSO
description: |
'This query detects when there is a spike in Microsoft Entra ID Seamless SSO errors. They may not be caused by a Password Spray attack, but the cause of the errors might need to be investigated.
Microsoft Entra ID only logs the requests that matched existing accounts, thus there might have been unlogged requests for non-existing accounts.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let account_threshold = 5;
AADNonInteractiveUserSignInLogs
//| where ResultType == "81016"
| where ResultType startswith "81"
| summarize DistinctAccounts = dcount(UserPrincipalName), DistinctAddresses = make_set(IPAddress,100) by ResultType
| where DistinctAccounts > account_threshold
| mv-expand IPAddress = DistinctAddresses
| extend IPAddress = tostring(IPAddress)
| join kind=leftouter (union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs) on IPAddress
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
UserPrincipalName = make_set(UserPrincipalName,100),
UserAgent = make_set(UserAgent,100),
ResultDescription = take_any(ResultDescription),
ResultSignature = take_any(ResultSignature)
by IPAddress, Type, ResultType
| project Type, StartTime, EndTime, IPAddress, ResultType, ResultDescription, ResultSignature, UserPrincipalName, UserAgent = iff(array_length(UserAgent) == 1, UserAgent[0], UserAgent)
| extend Name = tostring(split(UserPrincipalName[0],'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName[0],'@',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
Parameters
let account_threshold = 5;
Stage 1: source
AADNonInteractiveUserSignInLogs
Stage 2: where
| where ResultType startswith "81"
Stage 3: summarize
| summarize DistinctAccounts = dcount(UserPrincipalName), DistinctAddresses = make_set(IPAddress,100) by ResultType
Stage 4: where
| where DistinctAccounts > account_threshold
Stage 5: mv-expand
| mv-expand IPAddress = DistinctAddresses
Stage 6: extend
| extend IPAddress = tostring(IPAddress)
Stage 7: join
join kind=leftouter (...) on IPAddress
Stage 8: summarize
summarize EndTime, ResultDescription, ResultSignature, StartTime, UserAgent, UserPrincipalName by IPAddress, Type, ResultType
Stage 9: project
project EndTime, IPAddress, ResultDescription, ResultSignature, ResultType, StartTime, Type, UserAgent, UserPrincipalName
Stage 10: extend
extend Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DistinctAccounts | gt |
| field:"DistinctAccounts" kind:gt value:"5" |
ResultType | starts_with |
| field:"ResultType" kind:starts_with value:"81" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EndTime | project |
IPAddress | project |
ResultDescription | project |
ResultSignature | project |
ResultType | project |
StartTime | project |
Type | project |
UserAgent | project |
UserPrincipalName | project |
Name | extend |
UPNSuffix | extend |
Ping Federate - SAML old version
#'Detects requests using not the latest version of SAML protocol.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: fddd3840-acd2-41ed-94d9-1474b0a7c8a6
name: Ping Federate - SAML old version
description: |
'Detects requests using not the latest version of SAML protocol.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
query: |
PingFederateEvent
| where isnotempty(DeviceCustomString3)
| extend proto = extract(@'(SAML)', 1, DeviceCustomString3)
| extend ver = extract(@'(\d+)', 1, DeviceCustomString3)
| where proto =~ 'SAML'
| where ver !~ '20'
| extend AccountCustomEntity = DstUserName
| extend IpCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
PingFederateEvent
Stage 2: where
| where isnotempty(DeviceCustomString3)
Stage 3: extend
| extend proto = extract(@'(SAML)', 1, DeviceCustomString3)
Stage 4: extend
| extend ver = extract(@'(\d+)', 1, DeviceCustomString3)
Stage 5: where
| where proto =~ 'SAML'
Stage 6: where
| where ver !~ '20'
Stage 7: extend
| extend AccountCustomEntity = DstUserName
Stage 8: extend
| extend IpCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceCustomString3 | is_not_null | field:"DeviceCustomString3" kind:is_not_null | |
proto | eq |
| field:"proto" kind:eq value:"SAML" |
ver | ne |
| field:"ver" kind:ne value:"20" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
proto | extend |
ver | extend |
AccountCustomEntity | extend |
IpCustomEntity | extend |
Potential re-named sdelete usage
#This detection looks for command line parameters associated with the use of Sysinternals sdelete (https://docs.microsoft.com/sysinternals/downloads/sdelete) to delete multiple files on a host's C drive. A threat actor may re-name the tool to avoid detection and then use it for destructive attacks on a host.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Impact |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4688: A new process has been created. |
Rule Body
id: 720d12c6-a08c-44c4-b18f-2236412d59b0
name: Potential re-named sdelete usage
description: |
'This detection looks for command line parameters associated with the use of Sysinternals sdelete (https://docs.microsoft.com/sysinternals/downloads/sdelete) to delete multiple files on a host's C drive.
A threat actor may re-name the tool to avoid detection and then use it for destructive attacks on a host.'
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
- Impact
relevantTechniques:
- T1485
- T1036
query: |
SecurityEvent
| where EventID == 4688
| where Process !~ "sdelete.exe"
| where CommandLine has_all ("accepteula", "-r", "-s", "-q", "c:/")
| where CommandLine !has ("sdelete")
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(TargetAccount, @'\')[1]), AccountNTDomain = tostring(split(TargetAccount, @'\')[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetAccount
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 4688
Stage 3: where
| where Process !~ "sdelete.exe"
Stage 4: where
| where CommandLine has_all ("accepteula", "-r", "-s", "-q", "c:/")
Stage 5: where
| where CommandLine !has ("sdelete")
Stage 6: extend (3 consecutive steps)
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(TargetAccount, @'\')[1]), AccountNTDomain = tostring(split(TargetAccount, @'\')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
Process | ne |
| field:"Process" kind:ne value:"sdelete.exe" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
CommandLine | match | sdelete | excludes:CommandLine field:"CommandLine" value:"sdelete" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
PROD (TM011.1) - LAPS - Tier Level Computer Object LAPS Password Expiration Time Set Manually
#Alert if a Tier Model computer object LAPS password was manually expired.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5136: A directory service object was modified. |
Rule Body
id: daf2b9f7-dcf2-4b45-831a-d7142318c7e9
name: 'PROD (TM011.1) - LAPS - Tier Level Computer Object LAPS Password Expiration Time Set Manually'
description: |
Alert if a Tier Model computer object LAPS password was manually expired.
severity: Low
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Discovery
relevantTechniques:
- T1087.001
query: |
SecurityEvent
| where EventID == 5136
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers")
and AccountType == "User"
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| where AttributeLDAP has "msLAPS-PasswordExpirationTime"
| extend ObjectName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
| extend ObjectTier = case(
ObjectName_e has "Tier 0", "Tier 0",
ObjectName_e has "Tier 1", "Tier 1",
ObjectName_e has "Tier Model Administration", "Tier 0",
ObjectName_e has "Tier Model Computer Quarantine", "Tier 0",
ObjectName_e has "Domain Controllers", "Tier 0",
"Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))
| project TimeGenerated
, ObjectName
, ObjectTier
, AttributeLDAP
, ActivityType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ObjectName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
customDetails:
ObjectName: ObjectName
ObjectTier: ObjectTier
AttributeLDAP: AttributeLDAP
ActivityType: ActivityType
Account: Account
Domain: Domain
Computer: Computer
Channel: Channel
EventID: EventID
EventData: EventData
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: '(TM011.1) A {{ObjectTier}} LAPS password was expired by {{Account}} manually.'
alertDescriptionFormat: 'The LAPS password for {{ObjectName}} was expired manually.'
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5136
and (EventData matches regex @"(?i)OU=Domain Controllers"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW"
or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
or EventData matches regex @"(?i)OU=Tier 1 Member Servers")
and AccountType == "User"
Stage 3: extend
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
Stage 4: where
| where AttributeLDAP has "msLAPS-PasswordExpirationTime"
Stage 5: extend (6 consecutive steps)
| extend ObjectName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
| extend ObjectTier = case(
ObjectName_e has "Tier 0", "Tier 0",
ObjectName_e has "Tier 1", "Tier 1",
ObjectName_e has "Tier Model Administration", "Tier 0",
ObjectName_e has "Tier Model Computer Quarantine", "Tier 0",
ObjectName_e has "Domain Controllers", "Tier 0",
"Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))
Stage 6: project
| project TimeGenerated
, ObjectName
, ObjectTier
, AttributeLDAP
, ActivityType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountType | eq |
| field:"AccountType" kind:eq value:"User" |
AttributeLDAP | match |
| field:"AttributeLDAP" kind:match value:"msLAPS-PasswordExpirationTime" |
EventData | regex_match |
| field:"EventData" kind:regex_match |
EventID | eq |
| field:"EventID" kind:eq value:"5136" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
AttributeLDAP | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
ObjectName | project |
ObjectTier | project |
TimeGenerated | project |
Samsung Knox - Peripheral Access Detection with Mic Events
#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.
When microphone access has been detected on a Knox device, even though such access is disabled through an MDM device policy.
Rule Body
id: e4032fd2-4d05-4302-b7c0-f3f0380e2313
name: Samsung Knox - Peripheral Access Detection with Mic Events
version: 1.0.2
kind: NRT
description: When microphone access has been detected on a Knox device, even though such access is disabled through an MDM device policy.
severity: High
status: Available
requiredDataConnectors:
- connectorId: SamsungDCDefinition
dataTypes:
- Samsung_Knox_System_CL
tactics: []
relevantTechniques: []
query: |
Samsung_Knox_System_CL
| where Name == "PERIPHERAL_ACCESS_THROUGH_POLICY_DETECTED_MIC"
and MitreTtp has "KNOX.2"
alertDetailsOverride:
alertDynamicProperties: []
suppressionEnabled: false
suppressionDuration: PT5H
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
properties:
schema:
- "Name"
- "MitreTtp"
Stages and Predicates
Stage 1: source
Samsung_Knox_System_CL
Stage 2: where
| where Name == "PERIPHERAL_ACCESS_THROUGH_POLICY_DETECTED_MIC"
and MitreTtp has "KNOX.2"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
MitreTtp | match |
| field:"MitreTtp" kind:match value:"KNOX.2" |
Name | eq |
| field:"Name" kind:eq value:"PERIPHERAL_ACCESS_THROUGH_POLICY_DETECTED_MIC" |
Sentinel One - Rule disabled
#'Detects when a rule was disabled.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Rule Body
id: 84e210dd-8982-4398-b6f3-264fd72d036c
name: Sentinel One - Rule disabled
description: |
'Detects when a rule was disabled.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SentinelOne
dataTypes:
- SentinelOne
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1070
query: |
SentinelOne
| where ActivityType == 3603
| project EventCreationTime, DataRuleName, DataRuleQueryDetails, DataUserName
| extend AccountCustomEntity = DataUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SentinelOne
Stage 2: where
| where ActivityType == 3603
Stage 3: project
| project EventCreationTime, DataRuleName, DataRuleQueryDetails, DataUserName
Stage 4: extend
| extend AccountCustomEntity = DataUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityType | eq |
| field:"ActivityType" kind:eq value:"3603" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataRuleName | project |
DataRuleQueryDetails | project |
DataUserName | project |
EventCreationTime | project |
AccountCustomEntity | extend |
Silk Typhoon New UM Service Child Process
#This query looks for new processes being spawned by the Exchange UM service where that process has not previously been observed before. Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4688: A new process has been created. |
Rule Body
id: 95a15f39-d9cc-4667-8cdd-58f3113691c9
name: Silk Typhoon New UM Service Child Process
description: |
'This query looks for new processes being spawned by the Exchange UM service where that process has not previously been observed before.
Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
query: |
let lookback = 14d;
let timeframe = 1d;
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| join kind=rightanti (
SecurityEvent
| where TimeGenerated > ago(timeframe)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| where EventID == 4688) on NewProcessName
),
(WindowsEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688 and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| join kind=rightanti (
WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688 and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)) on NewProcessName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend SubjectUserName = tostring(EventData.SubjectUserName), SubjectDomainName = tostring(EventData.SubjectDomainName)
| project-away DomainIndex
))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SubjectAccount
- identifier: Name
columnName: SubjectUserName
- identifier: NTDomain
columnName: SubjectDomainName
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.1.6
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Threat Intelligence" ]
Stages and Predicates
Parameters
let lookback = 14d;
let timeframe = 1d;
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent
Leg 1: SecurityEvent
SecurityEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| join kind=rightanti (
SecurityEvent
| where TimeGenerated > ago(timeframe)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| where EventID == 4688) on NewProcessName
Leg 2: WindowsEvent
WindowsEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688 and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| join kind=rightanti (
WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688 and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)) on NewProcessName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend SubjectUserName = tostring(EventData.SubjectUserName), SubjectDomainName = tostring(EventData.SubjectDomainName)
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
ParentProcessName | match |
| field:"ParentImage" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ParentProcessName | extend |
NewProcessName | extend |
Account | extend |
IpAddress | extend |
HostName | extend |
HostNameDomain | extend |
SubjectDomainName | extend |
SubjectUserName | extend |
Speculus - Sign-in attempt from high-risk IP indicator
#Matches Microsoft Entra ID sign-in attempts against high-risk Speculus IP indicators (risk score >= 75) received in the last 14 days. A sign-in from a known C2, botnet, or brute-force source IP may indicate credential theft or account takeover.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Sign-in record (SigninLogs, any result) |
Rule Body
id: e1b8d4a6-9c27-45f3-8a1e-7f2c0d9b5e64
name: Speculus - Sign-in attempt from high-risk IP indicator
description: |
'Matches Microsoft Entra ID sign-in attempts against high-risk Speculus IP indicators (risk score >= 75)
received in the last 14 days. A sign-in from a known C2, botnet, or brute-force source IP may indicate
credential theft or account takeover.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SpeculusThreatIntel
dataTypes:
- Speculus_Indicators_CL
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
let indicators = Speculus_Indicators_CL
| where TimeGenerated > ago(14d)
| where isnotempty(IndicatorValue)
| where RiskScore >= 75
| summarize arg_max(Modified, *) by IndicatorValue
| project
IndicatorValue,
RiskScore,
SpeculusActivity = Activity,
Attribution,
Labels,
IndicatorDescription = Description;
SigninLogs
| where TimeGenerated > ago(1h)
| where isnotempty(IPAddress)
| join kind=inner indicators on $left.IPAddress == $right.IndicatorValue
| project
TimeGenerated,
IPAddress,
UserPrincipalName,
UserDisplayName,
AppDisplayName,
ResultType,
ResultDescription,
RiskScore,
SpeculusActivity,
Attribution,
Labels,
IndicatorDescription
suppressionDuration: 1h
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1d
matchingMethod: Selected
groupByEntities:
- Account
- IP
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
RiskScore: RiskScore
Attribution: Attribution
Labels: Labels
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
version: 1.0.0
kind: Scheduled
Stages and Predicates
Let Binding: indicators
let indicators = Speculus_Indicators_CL
| where TimeGenerated > ago(14d)
| where isnotempty(IndicatorValue)
| where RiskScore >= 75
| summarize arg_max(Modified, *) by IndicatorValue
| project
IndicatorValue,
RiskScore,
SpeculusActivity = Activity,
Attribution,
Labels,
IndicatorDescription = Description;
Stage 1: source
SigninLogs
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: where
| where isnotempty(IPAddress)
Stage 4: join
| join kind=inner indicators on $left.IPAddress == $right.IndicatorValue
Stage 5: project
project AppDisplayName, Attribution, IPAddress, IndicatorDescription, Labels, ResultDescription, ResultType, RiskScore, SpeculusActivity, TimeGenerated, UserDisplayName, UserPrincipalName
Stage 6: summarize aggregation inside the join branch
summarize by IndicatorValue
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IPAddress | is_not_null | field:"IPAddress" kind:is_not_null | |
IndicatorValue | is_not_null | field:"IndicatorValue" kind:is_not_null | |
RiskScore | ge |
| field:"RiskScore" kind:ge value:"75" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IndicatorValue | summarize |
Threat Essentials - Mass Cloud resource deletions Time Series Anomaly
#'This query generates baseline pattern of cloud resource deletions by an user and generated anomaly when any unusual spike is detected. These anomalies from unusual or privileged users could be an indication of cloud infrastructure take-down by an adversary '
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: fa2658fe-3714-4c55-bb12-2b7275c628e8
name: Threat Essentials - Mass Cloud resource deletions Time Series Anomaly
description: |
'This query generates baseline pattern of cloud resource deletions by an user and generated anomaly when any unusual spike is detected.
These anomalies from unusual or privileged users could be an indication of cloud infrastructure take-down by an adversary '
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1485
tags:
- DEV-0537
query: |
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 25;
let TimeSeriesData =
AzureActivity
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where OperationNameValue endswith "delete"
| project TimeGenerated, Caller
| make-series Total = count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Caller;
let TimeSeriesAlerts = materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 3, -1, 'linefit')
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
| where anomalies > 0
| project Caller, TimeGenerated, Total, baseline, anomalies, score
| where Total > TotalEventsThreshold and baseline > 0 );
TimeSeriesAlerts
| where TimeGenerated > (ago(endtime))
| project TimeGenerated, Caller
| join kind = inner (AzureActivity
| where TimeGenerated > (ago(endtime))
| where OperationNameValue endswith "delete"
| summarize count(), make_set(OperationNameValue), make_set(Resource) by bin(TimeGenerated, 1h), Caller) on TimeGenerated, Caller
| extend Name = iif(Caller has '@',tostring(split(Caller,'@',0)[0]),"")
| extend UPNSuffix = iif(Caller has '@',tostring(split(Caller,'@',1)[0]),"")
| extend AadUserId = iif(Caller !has '@',Caller,"")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- identifier: AadUserId
columnName: AadUserId
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 25;
let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.
Stages 1 to 10 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 11 to 14 use it.
Stage 1: source
AzureActivity
Stage 2: where
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
Stage 3: where
| where OperationNameValue endswith "delete"
Stage 4: project
| project TimeGenerated, Caller
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 5: make-series
| make-series Total = count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Caller
Stage 6: extend
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 3, -1, 'linefit')
Stage 7: mv-expand
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
Stage 8: where
| where anomalies > 0
Stage 9: project
| project Caller, TimeGenerated, Total, baseline, anomalies, score
Stage 10: where
| where Total > TotalEventsThreshold and baseline > 0
Stage 11: where
TimeSeriesAlerts
| where TimeGenerated > (ago(endtime))
Stage 12: project
| project TimeGenerated, Caller
Stage 13: join
| join kind = inner (AzureActivity
| where TimeGenerated > (ago(endtime))
| where OperationNameValue endswith "delete"
| summarize count(), make_set(OperationNameValue), make_set(Resource) by bin(TimeGenerated, 1h), Caller) on TimeGenerated, Caller
Stage 14: extend (3 consecutive steps)
extend AadUserId, Name, UPNSuffix
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Caller | project |
TimeGenerated | project |
Name | extend |
UPNSuffix | extend |
AadUserId | extend |
Varonis - High severity alerts detected
#'Identifies high-severity alerts generated by Varonis that indicate high-risk security events requiring immediate triage, investigation, and response.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
Rule Body
id: 7d2c9a41-5b8e-4f36-9c1a-2e6b8d4f7a13
name: Varonis - High severity alerts detected
description: |
'Identifies high-severity alerts generated by Varonis that indicate high-risk security events requiring immediate triage, investigation, and response.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: VaronisSaaSAlertsPush
dataTypes:
- VaronisAlertsV2_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- Exfiltration
relevantTechniques:
- T1530
- T1213
- T1567
query: |
VaronisAlertsV2_CL
| where AlertSeverity_s == "High"
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'Varonis alert: {{ThreatDetectionPolicyName_s}} ({{AlertSeverity_s}})'
alertDescriptionFormat: 'Varonis detected activity for policy {{ThreatDetectionPolicyName_s}} in category {{AlertCategory_s}}. Users: {{UserNames_s}}.'
customDetails:
VaronisAlertGuid: AlertId_g
AlertCategory: AlertCategory_s
MitreTactic: MitreTacticName_s
Status: Status_s
EventsCount: EventsCount_d
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserNames_s
- identifier: Name
columnName: UserSamAccountNames_s
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceNames_s
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
VaronisAlertsV2_CL
Stage 2: where
| where AlertSeverity_s == "High"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertSeverity_s | eq |
| field:"AlertSeverity_s" kind:eq value:"High" |
Veeam ONE Job Disabled (Veeam Backup for Microsoft 365)
#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 Veeam Backup for Microsoft 365 jobs are disabled.
Rule Body
id: 176abf3c-392a-47bd-b565-a4ecfd0ff7ec
name: Veeam ONE Job Disabled (Veeam Backup for Microsoft 365)
description: Detects when Veeam Backup for Microsoft 365 jobs are 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 == 378
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 == 378
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PredefinedAlarmId | eq |
| field:"PredefinedAlarmId" kind:eq value:"378" |
WDigest downgrade attack
#When the WDigest Authentication protocol is enabled, plain text passwords are stored in the Local Security Authority Subsystem Service (LSASS) exposing them to theft. This setting will prevent WDigest from storing credentials in memory. Ref: https://www.stigviewer.com/stig/windows_7/2016-12-19/finding/V-72753
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 13: RegistryEvent (Value Set) |
Rule Body
id: f6502545-ae3a-4232-a8b0-79d87e5c98d7
name: WDigest downgrade attack
description: |
'When the WDigest Authentication protocol is enabled, plain text passwords are stored in the Local Security Authority Subsystem Service (LSASS) exposing them to theft. This setting will prevent WDigest from storing credentials in memory.
Ref: https://www.stigviewer.com/stig/windows_7/2016-12-19/finding/V-72753'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1003
query: |
Event
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
| parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" *
| where TargetObject=~"HKLM\\System\\CurrentControlSet\\Control\\SecurityProviders\\WDigest\\UseLogonCredential" and Details !="DWORD (0x00000000)"
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer, TargetObject, Details
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
- entityType: RegistryKey
fieldMappings:
- identifier: Key
columnName: TargetObject
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
Event
Stage 2: where
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
Stage 3: parse
| parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" *
Stage 4: where
| where TargetObject=~"HKLM\\System\\CurrentControlSet\\Control\\SecurityProviders\\WDigest\\UseLogonCredential" and Details !="DWORD (0x00000000)"
Stage 5: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer, TargetObject, Details
Stage 6: extend
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Details | ne |
| field:"Details" kind:ne value:"DWORD (0x00000000)" |
EventID | in |
| field:"EventID" kind:in value:"13" |
EventLog | eq |
| field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational" |
TargetObject | eq |
| field:"TargetObject" kind:eq value:"HKLM\System\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
Details | summarize |
EndTimeUtc | summarize |
EventID | summarize |
StartTimeUtc | summarize |
TargetObject | summarize |
DnsDomain | extend |
HostName | extend |