Detection rules › Kusto

Kusto rules: 2h

RuleSeverity
[Entra ID] Domain Federation Trust Settings Modifiedhigh
Azure DevOps PAT used with Browsermedium
Azure DevOps Service Connection Addition/Abuse - Historic allow listmedium
BloodHound Attack Path Finding - Large Default Groups in DCOM Users Groupsmedium
BloodHound Attack Path Finding - WriteOwnerLimitedRights Privileges on Tier Zero Objectsmedium
BloodHound Attack Path Finding - WriteServicePrincipalName Privileges on Tier Zero Objectsmedium
Box - Inactive user loginmedium
Certified Pre-Owned - backup of CA private key - rule 1medium
Cloudflare - Empty user agentmedium
Corelight - External Proxy Detectedlow
Dataverse - Terminated employee exfiltration to USB drivehigh
Defender Alert Evidencehigh
Detect Custom Script or Run Command deployment by risky user
GCP IAM - New Authentication Token for Service Accountmedium
GCP Security Command Center - Detect DNSSEC disabled for DNS zonesmedium
Illumio Enforcement Change Analytic Rulemedium
MFA Rejected by Usermedium
NRT Azure DevOps Audit Stream Disabledhigh
OLE object manipulation attempts stateful anomaly on databasemedium
Oracle suspicious command executionmedium
Password spray attack against Microsoft Entra ID Seamless SSOmedium
Ping Federate - SAML old versionmedium
Potential re-named sdelete usagelow
PROD (TM011.1) - LAPS - Tier Level Computer Object LAPS Password Expiration Time Set Manuallylow
Samsung Knox - Peripheral Access Detection with Mic Eventshigh
Sentinel One - Rule disabledmedium
Silk Typhoon New UM Service Child Processmedium
Speculus - Sign-in attempt from high-risk IP indicatorhigh
Threat Essentials - Mass Cloud resource deletions Time Series Anomalymedium
Varonis - High severity alerts detectedhigh
Veeam ONE Job Disabled (Veeam Backup for Microsoft 365)medium
WDigest downgrade attackmedium

[Entra ID] Domain Federation Trust Settings Modified

#
Status
available
Severity
high
Time window
1d
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Persistence
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
AdditionalDetail.keyeq
  • User-Agent transforms: kusto_dynamic_path
field:"AdditionalDetail.key" kind:eq value:"User-Agent"
NewDomainValuematch
  • Federated transforms: term
field:"NewDomainValue" kind:match value:"Federated"
OperationNameeq
  • Set domain authentication corpus 3 (kusto 3)
  • Set federation settings on domain corpus 3 (kusto 3)
field:"OperationName" kind:eq
Property.displayNameeq
  • LiveType transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"LiveType"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Alert_Category_enproject
Alert_Containment_enproject
Alert_Description_enproject
Alert_Name_enproject
Alert_Remediation_enproject
Alert_SubCategory_enproject
Alert_Time_TWproject
Alert_Time_UTC0project
Alert_TriageStep_enproject
Event_Actionproject
Event_TimeRange_End_TWproject
Event_TimeRange_Start_TWproject
Source_Identity_DomainTypeproject
Source_Identity_FullNameproject
Source_Identity_Typeproject
Source_Network_IPAddressproject
Source_Network_IPLocationproject
Source_Resource_Nameproject
Target_Identity_IDproject
Target_Resource_IDproject
Target_Resource_Nameproject
Target_Resource_Typeproject

Azure DevOps PAT used with Browser

#
Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

FieldKindValuesSearch
AuthenticationMechanismstarts_with
  • PAT
field:"AuthenticationMechanism" kind:starts_with value:"PAT"
UserAgentmatch
  • Blink transforms: term
  • EdgeHTML transforms: term
  • Gecko transforms: term
  • Presto transforms: term
  • Trident transforms: term
  • WebKit transforms: term
field:"aws::userAgent" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameextend
AccountUPNSuffixextend

Azure DevOps Service Connection Addition/Abuse - Historic allow list

#
Status
available
Severity
medium
Time window
14d
Group by
ActorUPN, DefId, OrganizationName, ProjectId, ProjectName, Type
Source
github.com/Azure/Azure-Sentinel

'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

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 used in Stage 9

let BypassDefIds = datatable(DefId:string, Type:string, ProjectName:string)
[
];

Let Binding: HistoricDefs used in Stage 8

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 =
ifType == "Build"strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_build?definitionId=', DefId)
elsestrcat('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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUPNproject
DefIdproject
HistoricConnectionsproject
HistoricDistinctServiceConnectionsproject
OrganizationNameproject
ProjectNameproject
RecentConnectionsproject
RecentDistinctServiceConnectionsproject
StartTimeproject
linkproject
timestampextend
AccountNameextend
AccountUPNSuffixextend

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.

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
Findingeq
  • LargeDefaultGroupsDCOM
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.

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
Findingeq
  • T0WriteOwnerLimitedRights
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.

Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
Findingeq
  • T0WriteSPN
field:"Finding" kind:eq value:"T0WriteSPN"

Box - Inactive user login

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects user login after long inactivity period.'

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 3

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Certified Pre-Owned - backup of CA private key - rule 1

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

This query identifies someone that performs a read operation of they CA key from the file.

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Telemetry Coverage

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 =
ifOperation == "%%2458""Read persisted key from file"
elseOperation

Stage 9: extend

| extend Operation=iff(Operation == "%%2459", "Write persisted key to file", Operation)
Operation =
ifOperation == "%%2459""Write persisted key to file"
elseOperation

Stage 10: where

| where KeyName == "<INSERT ISSUING CA KEY HERE>"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Computercontains
  • <YOUR CA MACHINE NAME> corpus 2 (kusto 2)
field:"Computer" kind:contains value:"<YOUR CA MACHINE NAME>"
EventDatacontains
  • %%2499 corpus 2 (kusto 2)
field:"EventData" kind:contains value:"%%2499"
EventIDeq
  • 5058
field:"EventID" kind:eq value:"5058"
KeyNameeq
  • <INSERT ISSUING CA KEY HERE>
field:"KeyName" kind:eq value:"<INSERT ISSUING CA KEY HERE>"
d["@Name"]eq
  • KeyName transforms: kusto_dynamic_path
  • Operation transforms: kusto_dynamic_path
  • SubjectUserName transforms: kusto_dynamic_path

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventDataextend
Operationextend

Cloudflare - Empty user agent

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects requests where user agent is empty.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
HttpUserAgentOriginalis_null
  • (no value, null check)
field:"HttpUserAgentOriginal" kind:is_null

Corelight - External Proxy Detected

#
Status
available
Severity
low
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects external proxy usage.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
proxiedis_not_null
  • (no value, null check)
field:"proxied" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
id_orig_hcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:id_orig_h

Dataverse - Terminated employee exfiltration to USB drive

#
Status
available
Severity
high
Time window
14d
Group by
$left.InitiatingProcessAccountUpn, $left.InstanceUrl, $right.FileOriginUrl, $right.UserPrincipalName, DeviceId, InstanceUrl, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies files downloaded from Dataverse by departing or terminated employees which are copied to USB mounted drives.

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
CloudAppIdproject
Filesproject
InstanceUrlproject
LatestEventproject
PublicIPproject
UPNSuffixproject
UserIdproject

Defender Alert Evidence

#
Status
available
Severity
high
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Persistence

Telemetry Coverage

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.

FieldKindValuesSearch
EntityTypein
  • Device
  • User
field:"EntityType" kind:in

Detect Custom Script or Run Command deployment by risky user

#
Group by
$left.Caller, $right.UserPrincipalName, Caller, CallerIpAddress, CorrelationId, ResourceGroup, VMName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

References

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
CategoryValueeq
  • Administrative
field:"CategoryValue" kind:eq value:"Administrative"
OperationNameValueeq
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE
  • Microsoft.Compute/virtualMachines/runCommand/action corpus 3 (kusto 3)
field:"azure_ad::operation_name_value" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
Callersummarize
CallerIpAddresssummarize
CorrelationIdsummarize
ResourceGroupsummarize
TimeGeneratedsummarize
VMNamesummarize

GCP IAM - New Authentication Token for Service Account

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

Detects when new authentication token is created for service account.

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

PlatformRecord / event type
GCPGenerateAccessToken

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.

FieldKindValuesSearch
PayloadMethodnamein
  • GenerateAccessToken
  • google.iam.admin.v1.GenerateAccessToken
field:"gcp::method_name" kind:in
resulteq
  • true
field:"result" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
service_accountextend
AccountNameextend
AccountUPNSuffixextend

GCP Security Command Center - Detect DNSSEC disabled for DNS zones

#
Status
available
Severity
medium
Time window
1h
Group by
ExternalUri, ManagedZone, ProjectName, ResourceName, ResourceType, Severity
Source
github.com/Azure/Azure-Sentinel

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

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.

FieldKindValuesSearch
FindingCategoryeq
  • DNSSEC_DISABLED
field:"FindingCategory" kind:eq value:"DNSSEC_DISABLED"
Findings.stateeq
  • ACTIVE transforms: tostring, kusto_dynamic_path
field:"Findings.state" kind:eq value:"ACTIVE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ExternalUrisummarize
ManagedZonesummarize
ProjectNamesummarize
ResourceNamesummarize
ResourceTypesummarize
Severitysummarize
TimeGeneratedsummarize

Illumio Enforcement Change Analytic Rule

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Create Microsoft Sentinel Incident When Ven Changes Enforcement State from Full/Selective To Idle/Visibility state'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
TimeGeneratedproject
created_byproject
ipaddressproject
new_modeproject
old_modeproject
workload_hrefproject
workload_nameproject

MFA Rejected by User

#
Status
available
Severity
medium
Time window
1h
Group by
AADTenantId, FailedIPAddress, Name, UserId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADTenantIdsummarize
EndTImesummarize
FailedIPAddresssummarize
StartTimesummarize
UserIdsummarize
UserPrincipalNamesummarize
Nameextend
UPNSuffixextend
UEBARiskScoreextend

NRT Azure DevOps Audit Stream Disabled

#
Status
available
Severity
high
Source
github.com/Azure/Azure-Sentinel

'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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
StreamTypeextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

OLE object manipulation attempts stateful anomaly on database

#
Status
available
Severity
medium
Time window
14d
Group by
ApplicationName, ClientIp, Database, HostName, PrincipalName, ResourceId, timeSlice
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
Initial Access

Rules Detecting the Same Action

These rules filter on the same operation.

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 used in Stage 10

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 =
ifStatement has "hotwords"1
else0

Stage 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 =
if/* macro: (timeSlice >= ago(detectionWindow)) */'detection'
elif/* macro: (ago(trainingWindow) <= timeSlice) */'training'
else'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 =
ifcountStatementsWithHotwords >= monitoredStatementsThreshold and countSlicesWithHotwords <= trainingSlicesThresholdtrue
elsefalse

Stage 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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationNameproject
ClientIpproject
Databaseproject
HostNameproject
PrincipalNameproject
ResourceIdproject
TimeGeneratedproject
anomalyScoreproject
anyInfoproject
anyMonitoredStatementproject
countEventsproject
countStatementsproject
countStatementsWithHotwordsproject
hotWordproject
Nameextend
UPNSuffixextend

Oracle suspicious command execution

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

The query searches process creation events that are indicative of an attacker spawning OS commands from an Oracle database.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation
Lateral Movement

Telemetry Coverage

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
FileNameinconhost.exe, oradim.exeexcludes:FileName field:"FileName" value:"conhost.exe" field:"FileName" value:"oradim.exe"

Password spray attack against Microsoft Entra ID Seamless SSO

#
Status
available
Severity
medium
Time window
1h
Group by
IPAddress, ResultType, Type
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Credential Access

Telemetry Coverage

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimeproject
IPAddressproject
ResultDescriptionproject
ResultSignatureproject
ResultTypeproject
StartTimeproject
Typeproject
UserAgentproject
UserPrincipalNameproject
Nameextend
UPNSuffixextend

Ping Federate - SAML old version

#
Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'Detects requests using not the latest version of SAML protocol.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
protoextend
verextend
AccountCustomEntityextend
IpCustomEntityextend

Potential re-named sdelete usage

#
Status
available
Severity
low
Time window
1h
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Stealth
Impact

Telemetry Coverage

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.

FieldKindValuesSearch
CommandLinematch
  • -q transforms: term corpus 2 (kusto 2)
  • -r transforms: term corpus 2 (kusto 2)
  • -s transforms: term corpus 2 (kusto 2)
  • accepteula transforms: term corpus 3 (kusto 3)
  • c:/ transforms: term
field:"CommandLine" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
Processne
  • sdelete.exe
field:"Process" kind:ne value:"sdelete.exe"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CommandLinematchsdeleteexcludes:CommandLine field:"CommandLine" value:"sdelete"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

PROD (TM011.1) - LAPS - Tier Level Computer Object LAPS Password Expiration Time Set Manually

#
Status
available
Severity
low
Time window
5h
Source
github.com/Azure/Azure-Sentinel

Alert if a Tier Model computer object LAPS password was manually expired.

MITRE ATT&CK Coverage

Telemetry Coverage

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.

FieldKindValuesSearch
AccountTypeeq
  • User corpus 4 (kusto 4)
field:"AccountType" kind:eq value:"User"
AttributeLDAPmatch
  • msLAPS-PasswordExpirationTime transforms: term
field:"AttributeLDAP" kind:match value:"msLAPS-PasswordExpirationTime"
EventDataregex_match
  • (?i)OU=Domain Controllers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 1 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 1 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 2 PAW corpus 6 (kusto 6)
field:"EventData" kind:regex_match
EventIDeq
  • 5136 corpus 20 (kusto 18, elastic 2)
field:"EventID" kind:eq value:"5136"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
AttributeLDAPproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
ObjectNameproject
ObjectTierproject
TimeGeneratedproject

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.

Status
available
Severity
high
Source
github.com/Azure/Azure-Sentinel

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.

FieldKindValuesSearch
MitreTtpmatch
  • KNOX.2 transforms: term
field:"MitreTtp" kind:match value:"KNOX.2"
Nameeq
  • PERIPHERAL_ACCESS_THROUGH_POLICY_DETECTED_MIC
field:"Name" kind:eq value:"PERIPHERAL_ACCESS_THROUGH_POLICY_DETECTED_MIC"

Sentinel One - Rule disabled

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects when a rule was disabled.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataRuleNameproject
DataRuleQueryDetailsproject
DataUserNameproject
EventCreationTimeproject
AccountCustomEntityextend

Silk Typhoon New UM Service Child Process

#
Severity
medium
Time window
14d
Group by
NewProcessName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Initial Access

Telemetry Coverage

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.

FieldKindValuesSearch
EventDatamatch
  • UMService.exe transforms: term
  • umworkerprocess.exe transforms: term
field:"EventData" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
ParentProcessNamematch
  • UMService.exe transforms: term
  • UMService.exe transforms: term
  • umworkerprocess.exe transforms: term
  • umworkerprocess.exe transforms: term
field:"ParentImage" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ParentProcessNameextend
NewProcessNameextend
Accountextend
IpAddressextend
HostNameextend
HostNameDomainextend
SubjectDomainNameextend
SubjectUserNameextend

Speculus - Sign-in attempt from high-risk IP indicator

#
Status
available
Severity
high
Time window
14d
Group by
$left.IPAddress, $right.IndicatorValue, IndicatorValue
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

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 used in Stage 4

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.

FieldKindValuesSearch
IPAddressis_not_null
  • (no value, null check)
field:"IPAddress" kind:is_not_null
IndicatorValueis_not_null
  • (no value, null check)
field:"IndicatorValue" kind:is_not_null
RiskScorege
  • 75 transforms: number
field:"RiskScore" kind:ge value:"75"

Output Fields

These fields are emitted when the rule matches.

FieldSource
IndicatorValuesummarize

Threat Essentials - Mass Cloud resource deletions Time Series Anomaly

#
Status
available
Severity
medium
Time window
14d
Group by
Caller, TimeGenerated
Source
github.com/Azure/Azure-Sentinel

'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

TacticTechniques
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.

FieldSource
Callerproject
TimeGeneratedproject
Nameextend
UPNSuffixextend
AadUserIdextend

Varonis - High severity alerts detected

#
Status
available
Severity
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Identifies high-severity alerts generated by Varonis that indicate high-risk security events requiring immediate triage, investigation, and response.'

MITRE ATT&CK Coverage

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.

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.

Status
available
Severity
medium
Time window
3h
Source
github.com/Azure/Azure-Sentinel

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.

WDigest downgrade attack

#
Status
available
Severity
medium
Time window
1h
Group by
Computer, Details, EventID, TargetObject
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Credential Access

Telemetry Coverage

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.

FieldKindValuesSearch
Detailsne
  • DWORD (0x00000000)
field:"Details" kind:ne value:"DWORD (0x00000000)"
EventIDin
  • 13 corpus 4 (kusto 4)
field:"EventID" kind:in value:"13"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 7 (kusto 7)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
TargetObjecteq
  • HKLM\System\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential
field:"TargetObject" kind:eq value:"HKLM\System\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
Detailssummarize
EndTimeUtcsummarize
EventIDsummarize
StartTimeUtcsummarize
TargetObjectsummarize
DnsDomainextend
HostNameextend

Back to all Kusto rules