Detection rules › Kusto

Kusto rules: 0l

1Password - Privileged vault permission change

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

This will alert when permissions have changed within a privileged vault. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 76e386eb-f51a-4600-97d1-f0db3b7e41f1
name: 1Password - Privileged vault permission change
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when permissions have changed within a privileged vault. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.

  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: High
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1098
query: |-
  let watchlist =
      _GetWatchlist("PV1PW")
      | project SearchKey
  ;
  // Insert the vault UUIDs below when using the dynamic vaults list within the analytics rule itself
  let vaults = dynamic([""]);
  OnePasswordEventLogs_CL
  | where log_source == "auditevents"
  | where
      (action has_any("grant", "revoke", "update") and object_type == "uva") or
      (action has_any("grant", "revoke", "update") and object_type == "gva")
  // Enable the line below when using the "Privileged Vaults - 1PW" watchlist
  | where object_uuid in (watchlist)
  // Enable the line below when using the dynamic vaults list within the analytics rule itself
  // | where object_uuid in (vaults)
  | extend
      TargetUsername = case(isnotempty(aux_details), aux_details.email, "")
      , TargetGroupUUID = case(isempty(aux_details), aux_uuid, "")
      , ActorUsername = actor_details.email
      , SrcIpAddr = session.ip
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: TargetUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Parameters

let vaults = dynamic([""]);

Let Binding: watchlist used in Stage 4

let watchlist = _GetWatchlist("PV1PW")
    | project SearchKey;

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where log_source == "auditevents"

Stage 3: where

| where
    (action has_any("grant", "revoke", "update") and object_type == "uva") or
    (action has_any("grant", "revoke", "update") and object_type == "gva")

Stage 4: where

| where object_uuid in (watchlist)

Stage 5: extend

| extend
    TargetUsername = case(isnotempty(aux_details), aux_details.email, "")
    , TargetGroupUUID = case(isempty(aux_details), aux_uuid, "")
    , ActorUsername = actor_details.email
    , SrcIpAddr = session.ip
TargetGroupUUID =
ifisempty(aux_details)aux_uuid
else""
TargetUsername =
ifisnotempty(aux_details)aux_details.email
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionmatch
  • grant transforms: term corpus 4 (kusto 4)
  • revoke transforms: term corpus 2 (kusto 2)
  • update transforms: term corpus 3 (kusto 3)
field:"action" kind:match
log_sourceeq
  • auditevents corpus 12 (kusto 12)
field:"log_source" kind:eq value:"auditevents"
object_typeeq
  • gva
  • uva corpus 4 (kusto 4)
field:"object_type" kind:eq
object_uuidin
  • watchlist
field:"object_uuid" kind:in value:"watchlist"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
SrcIpAddrextend
TargetGroupUUIDextend
TargetUsernameextend

[Entra ID] Mass Privileged Role Change Activity Detected

#
Status
available
Severity
high
Time window
1h
Group by
$left.TimeGenerated, $right.TimeWindow
Source
github.com/Azure/Azure-Sentinel

Detects multiple privileged role changes occurring around the same time. This may signal bulk abuse or administrative misconfiguration.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: d6cd3c6f-1d2a-4c42-a048-dbe91cf35b18
name: "[Entra ID] Mass Privileged Role Change Activity Detected"
version: 1.0.0
kind: Scheduled
description: |
  Detects multiple privileged role changes occurring around the same time. This may signal bulk abuse or administrative misconfiguration.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
  AuditLogs
  | where Category =~ "RoleManagement"
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
  | mv-expand TargetResources
  | mv-expand TargetResources.modifiedProperties
  | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
  | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
  | extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.servicePrincipalName)
  | where RoleName contains "Admin"
  | extend Target = tostring(TargetResources.userPrincipalName)
  | summarize dcount(Target) by bin(TimeGenerated, 1h)
  | where dcount_Target >= 9
  | join kind=rightsemi  (AuditLogs
      | where Category =~ "RoleManagement"
      | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
      | mv-expand TargetResources
      | mv-expand TargetResources.modifiedProperties
      | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
      | where displayName_ =~ "Role.DisplayName"
      | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
      | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
      | extend Source_UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
      | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
      | where RoleName contains "Admin"
      | extend Target = tostring(TargetResources.userPrincipalName)
      | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
      | extend Source_Network_IPLocation = ""
      | extend TimeWindow = bin(TimeGenerated, 1h))
      on $left.TimeGenerated == $right.TimeWindow
  | project
          Alert_Category_en = "Entra ID",
      Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
      Alert_Name_en = "Mass Privileged Role Change Activity Detected",
      Alert_Description_en =  strcat(
                              "At Taiwan time: ",
                              format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
                              "in the Microsoft Entra ID tenant"
      ", user:  ",
                              iff(isnotempty(Source_UserPrincipalName), Source_UserPrincipalName, InitiatingAppName),
                              " at IP: ",
                              InitiatingIpAddress,
                              ", performed mass permission changes. Newly added role permission: ",
                              RoleName
                          ),
      Alert_TriageStep_en=
      strcat(
      "1. Confirm whether the high-privilege role assignment  ",
      iff(isnotempty(RoleName), RoleName, "<NoRole>"),
      "  has a corresponding change request or approval record.",
      "2. Verify whether target account  ",
      iff(isnotempty(Target), Target, "<NoTargetUser>"),
      "  is a user who actually needs this administrative permission.",
      "3. Check whether source IP  ",
      iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
      "  and sign-in location  ",
      iff(isnotempty(Source_Network_IPLocation), Source_Network_IPLocation, "<NoLocation>"),
      "  are company IPs and commonly used locations."
  ),
      Alert_Containment_en = strcat(
                             "1. Immediately review and suspend all Admin role members added during the event time window, confirm whether they are approved permission changes, and immediately remove unauthorized roles.",
                             "2. Immediately restrict or disable the source account or application that performed the role assignment (",
                             iff(isnotempty(InitiatingAppName), InitiatingAppName, "<NoInitiator>"),
                             "), and revoke its existing sign-in tokens and sessions. ",
                             "3. If the source is a Service Principal, immediately disable the application, revoke credentials/secrets and Graph API permissions, and prevent continued automation abuse. "
                         ),
      Alert_Remediation_en = strcat(
                             "1. Strengthen Admin role assignment governance: allow Just-In-Time access only through PIM, require approval and access justification, and prohibit permanent assignments. ",
                             "2. Establish real-time alerts and automated response (SOAR) for role changes, including mass assignments, off-hours activity, or abnormal geolocation behavior.",
                             "3. Regularly audit Admin role members/Service Principal permissions and API consent, and remove unnecessary or long-unused high-privilege access. "
                         ),
              Alert_Time_TW = datetime_utc_to_local(TimeGenerated, "Asia/Taipei"),
      Alert_Time_UTC0 = TimeGenerated,
      Event_Action = ActivityDisplayName,
      Event_Status = Result,
      //Event_Description = ActivityDisplayName,
      Source_Identity_FullName = iff(isnotempty(Source_UserPrincipalName), Source_UserPrincipalName, InitiatingAppName),
      //Source_Identity_ID = ActorID,
      Source_Identity_Type = iff(isnotempty(Source_UserPrincipalName), "User", "Service"),
      Source_Network_IPAddress = InitiatingIpAddress,
      Source_Network_IPLocation = Source_Network_IPLocation,
      //Source_Resource_Name = InitiatingAppName,
      Target_Identity_FullName = Target,
      Target_Identity_DomainType = iff(Target contains "EXT", 'External', 'Internal'),
      Target_Identity_Type = "User",
      Target_Resource_ID = "",
      Target_Resource_Type = "Entra ID"
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Source_Identity_FullName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Source_Network_IPAddress
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target_Identity_FullName
alertDetailsOverride:
  alertDynamicProperties: []

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "RoleManagement"

Stage 3: where

| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")

Stage 4: mv-expand

| mv-expand TargetResources

Stage 5: mv-expand

| mv-expand TargetResources.modifiedProperties

Stage 6: extend (3 consecutive steps)

| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
| extend InitiatingAppServicePrincipalName = tostring(InitiatedBy.app.servicePrincipalName)

Stage 7: where

| where RoleName contains "Admin"

Stage 8: extend

| extend Target = tostring(TargetResources.userPrincipalName)

Stage 9: summarize

| summarize dcount(Target) by bin(TimeGenerated, 1h)

Stage 10: where

| where dcount_Target >= 9

Stage 11: join

| join kind=rightsemi  (AuditLogs
    | where Category =~ "RoleManagement"
    | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
    | mv-expand TargetResources
    | mv-expand TargetResources.modifiedProperties
    | extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
    | where displayName_ =~ "Role.DisplayName"
    | extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
    | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
    | extend Source_UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
    | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
    | where RoleName contains "Admin"
    | extend Target = tostring(TargetResources.userPrincipalName)
    | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
    | extend Source_Network_IPLocation = ""
    | extend TimeWindow = bin(TimeGenerated, 1h))
    on $left.TimeGenerated == $right.TimeWindow

Stage 12: project

project Alert_Category_en, Alert_Containment_en, Alert_Description_en, Alert_Name_en, Alert_Remediation_en, Alert_SubCategory_en, Alert_Time_TW, Alert_Time_UTC0, Alert_TriageStep_en, Event_Action, Event_Status, Source_Identity_FullName, Source_Identity_Type, Source_Network_IPAddress, Source_Network_IPLocation, Target_Identity_DomainType, Target_Identity_FullName, Target_Identity_Type, Target_Resource_ID, Target_Resource_Type

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityDisplayNamematch
  • Add eligible member to role transforms: term corpus 9 (kusto 9)
  • Add eligible member to role transforms: term
  • Add member to role transforms: term corpus 9 (kusto 9)
  • Add member to role transforms: term
field:"azure_ad::activity_display_name" kind:match
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
RoleNamecontains
  • Admin
field:"RoleName" kind:contains value:"Admin"
dcount_Targetge
  • 9 transforms: number
field:"dcount_Target" kind:ge value:"9"
displayName_eq
  • Role.DisplayName
field:"displayName_" kind:eq value:"Role.DisplayName"

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_Statusproject
Source_Identity_FullNameproject
Source_Identity_Typeproject
Source_Network_IPAddressproject
Source_Network_IPLocationproject
Target_Identity_DomainTypeproject
Target_Identity_FullNameproject
Target_Identity_Typeproject
Target_Resource_IDproject
Target_Resource_Typeproject

AD user enabled and password not set within 48 hours

#
Status
available
Severity
low
Time window
3d
Group by
TargetAccount, TargetSid
Source
github.com/Azure/Azure-Sentinel

Identifies when an account is enabled with a default password and the password is not set by the user within 48 hours. Effectively, there is an event 4722 indicating an account was enabled and within 48 hours, no event 4723 occurs which indicates there was no attempt by the user to set the password. This will show any attempts (success or fail) that occur after 48 hours, which can indicate too long of a time period in setting the password to something that only the user knows. It is recommended that this time period is adjusted per your internal company policy.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 62085097-d113-459f-9ea7-30216f2ee6af
name: AD user enabled and password not set within 48 hours
description: |
  'Identifies when an account is enabled with a default password and the password is not set by the user within 48 hours.
  Effectively, there is an event 4722 indicating an account was enabled and within 48 hours, no event 4723 occurs which
  indicates there was no attempt by the user to set the password. This will show any attempts (success or fail) that occur
  after 48 hours, which can indicate too long of a time period in setting the password to something that only the user knows.
  It is recommended that this time period is adjusted per your internal company policy.'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 3d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  let starttime = 3d;
  let SecEvents = materialize ( SecurityEvent | where TimeGenerated >= ago(starttime)
  | where EventID in (4722,4723) | where TargetUserName !endswith "$"
  | project TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetSid, SubjectAccount, SubjectUserSid);
  let userEnable = SecEvents
  | extend EventID4722Time = TimeGenerated
  // 4722: User Account Enabled
  | where EventID == 4722
  | project Time_Event4722 = TimeGenerated, TargetAccount, TargetSid, SubjectAccount_Event4722 = SubjectAccount, SubjectUserSid_Event4722 = SubjectUserSid, Activity_4722 = Activity, Computer_4722 = Computer;
  let userPwdSet = SecEvents
  // 4723: Attempt made by user to set password
  | where EventID == 4723
  | project Time_Event4723 = TimeGenerated, TargetAccount, TargetSid, SubjectAccount_Event4723 = SubjectAccount, SubjectUserSid_Event4723 = SubjectUserSid, Activity_4723 = Activity, Computer_4723 = Computer;
  userEnable | join kind=leftouter userPwdSet on TargetAccount, TargetSid
  | extend PasswordSetAttemptDelta_Min = datetime_diff('minute', Time_Event4723, Time_Event4722)
  | where PasswordSetAttemptDelta_Min > 2880 or isempty(PasswordSetAttemptDelta_Min)
  | project-away TargetAccount1, TargetSid1
  | extend Reason = @"User either has not yet attempted to set the initial password after account was enabled or it occurred after 48 hours"
  | order by Time_Event4722 asc
  | project-reorder Time_Event4722, Time_Event4723, PasswordSetAttemptDelta_Min, TargetAccount, TargetSid
  | extend Computer = coalesce(Computer_4723, Computer_4722)
  | 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])
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings: 
      - identifier: FullName
        columnName: TargetAccount
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: TargetSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 3d;

let SecEvents and let userEnable are inlined into the numbered stages below.

Let Binding: userPwdSet used in Stages 3, 12

let userPwdSet = SecEvents
| where EventID == 4723
| project Time_Event4723 = TimeGenerated, TargetAccount, TargetSid, SubjectAccount_Event4723 = SubjectAccount, SubjectUserSid_Event4723 = SubjectUserSid, Activity_4723 = Activity, Computer_4723 = Computer;

Stages 1 to 11 define let userEnable as the rule's main pipeline source. Stages 12 to 20 use it.

Stage 1: source

let SecEvents

Stage 2: source

let userEnable

Stage 3: source

let userPwdSet

Stage 4: source

SecurityEvent

Stage 5: where

| where TimeGenerated >= ago(starttime)

Stage 6: where

| where EventID in (4722,4723)

Stage 7: where

| where TargetUserName !endswith "$"

Stage 8: project

| project TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetSid, SubjectAccount, SubjectUserSid

Stage 9: extend

| extend EventID4722Time = TimeGenerated

Stage 10: where

| where EventID == 4722

Stage 11: project

| project Time_Event4722 = TimeGenerated, TargetAccount, TargetSid, SubjectAccount_Event4722 = SubjectAccount, SubjectUserSid_Event4722 = SubjectUserSid, Activity_4722 = Activity, Computer_4722 = Computer

Stage 12: join

userEnable
| join kind=leftouter userPwdSet on TargetAccount, TargetSid

Stage 13: extend

extend PasswordSetAttemptDelta_Min

Stage 14: where

where (PasswordSetAttemptDelta_Min > 2880 or isempty(PasswordSetAttemptDelta_Min))

Stage 15: project-away

project-away TargetAccount1, TargetSid1

Stage 16: extend

extend Reason

Stage 17: sort

sort by Time_Event4722

Stage 18: project-reorder

project-reorder

Stage 19: extend (4 consecutive steps)

extend AccountNTDomain, AccountName, Computer, DomainIndex, HostName, HostNameDomain

Stage 20: project-away

project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TargetUserNameends_with$excludes:TargetUserName field:"TargetUserName" value:"$"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activity_4722project
Computer_4722project
SubjectAccount_Event4722project
SubjectUserSid_Event4722project
TargetAccountproject
TargetSidproject
Time_Event4722project
PasswordSetAttemptDelta_Minextend
Reasonextend
Computerextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

Attempt to bypass conditional access rule in Microsoft Entra ID

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

Identifies an attempt to Bypass conditional access rule(s) in Microsoft Entra ID. The ConditionalAccessStatus column value details if there was an attempt to bypass Conditional Access or if the Conditional access rule was not satisfied (ConditionalAccessStatus == 1). References: https://docs.microsoft.com/azure/active-directory/conditional-access/overview https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes ConditionalAccessStatus == 0 // Success ConditionalAccessStatus == 1 // Failure ConditionalAccessStatus == 2 // Not Applied ConditionalAccessStatus == 3 // unknown Note: This analytic rule is designed to identify potential attempts to bypass Conditional Access policies. Depending on your organization's configuration, some Conditional Access failures may be legitimate and expected. It is recommended to review alerts generated by this rule in the context of your organization's Conditional Access policies and adjust the detection threshold as needed to reduce false positives. This rule includes an optional filter that leverages a watchlist (ConditionalAccessBenignStatusCodes) to exclude known benign status codes. You can customize this watchlist with status codes that are expected in your environment and do not indicate malicious activity. By default, the watchlist-based filtering is commented out to provide full visibility into all relevant events. You may choose to enable this filter by uncommenting the corresponding line in the query to reduce noise from known benign scenarios.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 3af9285d-bb98-4a35-ad29-5ea39ba0c628
name: Attempt to bypass conditional access rule in Microsoft Entra ID
description: |
  'Identifies an attempt to Bypass conditional access rule(s) in Microsoft Entra ID.
  The ConditionalAccessStatus column value details if there was an attempt to bypass Conditional Access or if the Conditional access rule was not satisfied (ConditionalAccessStatus == 1).
  References:
  https://docs.microsoft.com/azure/active-directory/conditional-access/overview
  https://docs.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins
  https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes
  ConditionalAccessStatus == 0 // Success
  ConditionalAccessStatus == 1 // Failure
  ConditionalAccessStatus == 2 // Not Applied
  ConditionalAccessStatus == 3 // unknown
  Note:
  This analytic rule is designed to identify potential attempts to bypass Conditional Access policies. Depending on your organization's configuration, some Conditional Access failures may be legitimate and expected.
  It is recommended to review alerts generated by this rule in the context of your organization's Conditional Access policies and adjust the detection threshold as needed to reduce false positives.
  This rule includes an optional filter that leverages a watchlist (ConditionalAccessBenignStatusCodes) to exclude known benign status codes. You can customize this watchlist with status codes that are expected in your environment and do not indicate malicious activity.
  By default, the watchlist-based filtering is commented out to provide full visibility into all relevant events. You may choose to enable this filter by uncommenting the corresponding line in the query to reduce noise from known benign scenarios.'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1098
query: |
  let threshold = 1; // Modify this threshold value to reduce false positives based on your environment
  let aadFunc = (tableName:string){
  table(tableName)
  | where ConditionalAccessStatus == 1 or ConditionalAccessStatus =~ "failure"
  | mv-apply CAP = parse_json(ConditionalAccessPolicies) on (
    project ConditionalAccessPoliciesName = CAP.displayName, result = CAP.result
    | where result =~ "failure"
  )
  | extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
  | extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
  | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
  | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
  // Filter out status codes using watchlist
  // uncomment the following line to enable watchlist filtering
  // | join kind=leftanti (_GetWatchlist("ConditionalAccessBenignStatusCodes") | project StatusCode) on StatusCode
  | extend Status = strcat(StatusCode, ": ", ResultDescription)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Status = make_list(Status,10), StatusDetails = make_list(StatusDetails,50), IPAddresses = make_list(IPAddress,100), IPAddressCount = dcount(IPAddress), CorrelationIds = make_list(CorrelationId,100), ConditionalAccessPoliciesName = make_list(ConditionalAccessPoliciesName,100)
  by UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, Type
  | where IPAddressCount > threshold and StatusDetails !has "MFA successfully completed"
  | mv-expand IPAddresses, Status, StatusDetails, CorrelationIds
  | extend Status = strcat(Status, " ", StatusDetails)
  | summarize IPAddresses = make_set(IPAddresses,100), Status = make_set(Status,10), CorrelationIds = make_set(CorrelationIds,100), ConditionalAccessPoliciesName = make_set(ConditionalAccessPoliciesName,100)
  by StartTime, EndTime, UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, IPAddressCount, Type
  | extend IPAddressFirst = tostring(IPAddresses[0]), Name = tostring(split(UserPrincipalName, "@")[0]), UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddressFirst
version: 1.0.8
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 1;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName:string){
table(tableName)
| where ConditionalAccessStatus == 1 or ConditionalAccessStatus =~ "failure"
| mv-apply CAP = parse_json(ConditionalAccessPolicies) on (
  project ConditionalAccessPoliciesName = CAP.displayName, result = CAP.result
  | where result =~ "failure"
)
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
| extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
| extend Status = strcat(StatusCode, ": ", ResultDescription)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Status = make_list(Status,10), StatusDetails = make_list(StatusDetails,50), IPAddresses = make_list(IPAddress,100), IPAddressCount = dcount(IPAddress), CorrelationIds = make_list(CorrelationId,100), ConditionalAccessPoliciesName = make_list(ConditionalAccessPoliciesName,100)
by UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, Type
| where IPAddressCount > threshold and StatusDetails !has "MFA successfully completed"
| mv-expand IPAddresses, Status, StatusDetails, CorrelationIds
| extend Status = strcat(Status, " ", StatusDetails)
| summarize IPAddresses = make_set(IPAddresses,100), Status = make_set(Status,10), CorrelationIds = make_set(CorrelationIds,100), ConditionalAccessPoliciesName = make_set(ConditionalAccessPoliciesName,100)
by StartTime, EndTime, UserPrincipalName, UserId, AppDisplayName, tostring(Browser), tostring(OS), City, State, Region, IPAddressCount, Type
| extend IPAddressFirst = tostring(IPAddresses[0]), Name = tostring(split(UserPrincipalName, "@")[0]), UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt

Leg 1: aadSignin

Leg 2: aadNonInt

BloodHound Attack Path Finding - Contributor Role on Tier Zero Automation Account

#

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 - Contributor Role on Tier Zero Automation Account

Rule Body

id: 04a117f0-ff6e-464b-984d-119e33fb0dd5
name: BloodHound Attack Path Finding - Contributor Role on Tier Zero Automation Account
description: BloodHound Attack Path Finding - Contributor Role on Tier Zero Automation
  Account
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 == "AzureT0AutomationContributor"
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 == "AzureT0AutomationContributor"

Indicators

These rows show field, operator, and value matches.

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

Cisco SE High Events Last Hour

#
Status
available
Severity
high
Time window
1h
Group by
computer_hostname_s, computer_links_trajectory_s, date_t, event_type_s
Source
github.com/Azure/Azure-Sentinel

'Find events from Cisco Secure Endpoint that are of High severity in the last hour.'

MITRE ATT&CK Coverage

Rule Body

id: 4683ebce-07ad-4089-89e3-39d8fe83c011
name: Cisco SE High Events Last Hour
description: |
  'Find events from Cisco Secure Endpoint that are of High severity in the last hour.'
severity: High
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
  - InitialAccess
relevantTechniques:
  - T1204.002
  - T1190
query: |
  let endtime = 1h;
  CiscoSecureEndpoint_CL
  | where TimeGenerated >= ago(endtime)
  | where severity_s == "High"
  | project NetworkAddresses = parse_json(computer_network_addresses_s), computer_hostname_s, date_t, event_type_s, computer_links_trajectory_s
  | summarize CountInLastHour = count() by computer_hostname_s, date_t, event_type_s, tostring(NetworkAddresses.ip), computer_links_trajectory_s
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: NetworkAddresses_ip
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: computer_hostname_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: computer_links_trajectory_s
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let endtime = 1h;

Stage 1: source

CiscoSecureEndpoint_CL

Stage 2: where

| where TimeGenerated >= ago(endtime)

Stage 3: where

| where severity_s == "High"

Stage 4: project

| project NetworkAddresses = parse_json(computer_network_addresses_s), computer_hostname_s, date_t, event_type_s, computer_links_trajectory_s

Stage 5: summarize

| summarize CountInLastHour = count() by computer_hostname_s, date_t, event_type_s, tostring(NetworkAddresses.ip), computer_links_trajectory_s

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
severity_seq
  • High corpus 2 (kusto 2)
field:"okta::severity" kind:eq value:"High"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CountInLastHoursummarize
computer_hostname_ssummarize
computer_links_trajectory_ssummarize
date_tsummarize
event_type_ssummarize

Cisco WSA - Unscannable file or scan error

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

'Detects unscanned downloaded file.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 9b61a945-ebcb-4245-b6e4-51f3addb5248
name: Cisco WSA - Unscannable file or scan error
description: |
  'Detects unscanned downloaded file.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1189
query: |
  CiscoWSAEvent
  | where isnotempty(AmpFileName)
  | where AmpScanningVerdict in ('2', '3')
  | extend IPCustomEntity = SrcIpAddr, FileCustomEntity = AmpFileName, AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoWSAEvent

Stage 2: where

| where isnotempty(AmpFileName)

Stage 3: where

| where AmpScanningVerdict in ('2', '3')

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = AmpFileName, AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
FileCustomEntityextend
IPCustomEntityextend

Deimos Component Execution

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

Jupyter, otherwise known as SolarMarker, is a malware family and cluster of components known for its info-stealing and backdoor capabilities that mainly proliferates through search engine optimization manipulation and malicious advertising in order to successfully encourage users to download malicious templates and documents. This malware has been popular since 2020 and currently is still active as of 2021.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: c25a8cd4-5b4a-45a8-9ba0-3b753a652f6b
name: Deimos Component Execution
description: |
  Jupyter, otherwise known as SolarMarker, is a malware family and cluster of components known for its info-stealing and backdoor capabilities that mainly proliferates through search engine optimization manipulation and malicious advertising in order to successfully encourage users to download malicious templates and documents. This malware has been popular since 2020 and currently is still active as of 2021.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Collection
  - Exfiltration
relevantTechniques:
  - T1059
  - T1005
  - T1020
tags:
  - SolarMarker
  - Jupyter
  - Ransomware
query: |
  DeviceEvents   
  | where InitiatingProcessFileName =~ "powershell.exe"
  | where ActionType == "AmsiScriptContent"
  | where AdditionalFields endswith '[mArS.deiMos]::inteRaCt()"}'
  | project InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessCommandLine, ActionType, AdditionalFields, DeviceName
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceEvents

Stage 2: where

| where InitiatingProcessFileName =~ "powershell.exe"

Stage 3: where

| where ActionType == "AmsiScriptContent"

Stage 4: where

| where AdditionalFields endswith '[mArS.deiMos]::inteRaCt()"}'

Stage 5: project

| project InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessCommandLine, ActionType, AdditionalFields, DeviceName

Stage 6: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 7: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionTypeproject
AdditionalFieldsproject
DeviceNameproject
InitiatingProcessCommandLineproject
InitiatingProcessFileNameproject
InitiatingProcessParentFileNameproject
HostNameextend
DnsDomainextend

Detect AWS IAM Users

#

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
Time window
30m
Source
github.com/Azure/Azure-Sentinel

The policy detects IAM users across your AWS accounts, a practice that should be kept only for a small number of accounts. This is a default definition by Authomize and can be updated using the edit modal.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: 077eb06a-c011-47f7-8d92-dfc2b1e1d71b
name: Detect AWS IAM Users
kind: Scheduled
description: The policy detects IAM users across your AWS accounts, a practice that should be kept only for a small number of accounts. This is a default definition by Authomize and can be updated using the edit modal.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "Detect AWS IAM Users"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - Detect AWS IAM Users
  alertDescriptionFormat: Detect AWS IAM Users. The policy detects IAM users across your AWS accounts, a practice that should be kept only for a small number of accounts. This is a default definition by Authomize and can be updated using the edit modal.
  alertSeverity: Severity
  alertTactics: Tactics  
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "Detect AWS IAM Users"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • Detect AWS IAM Users transforms: term
field:"policy_name_s" kind:match value:"Detect AWS IAM Users"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Detect suspicious foci token logins

#
Group by
SessionId, UserPrincipalName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

FOCI tokens (Family of Client IDs tokens) are special refresh tokens that allow multiple applications within the same "family" to share authentication tokens. This means that once a user authenticates with one application, they can access other applications in the same family without needing to re-authenticate. For adversaries, these are very interesting tokens to abuse since they can access a normal application (Microsoft Teams for example), and reuse that refresh token to access another application (like Azure CLI). To detect a suspicious foci token combination, we look for all the logins using foci tokens and group them by Session ID (since these belong to the same session). Then we take the first login where no refresh token was provided, and look at the logins that used refresh tokens as incomming token types within that same session. If the second login application is one that is typically abused by adversaries and the application for the first login is a 'normal' application, we flag the event. We added a second version for this query in this repo, to also flag when an adversary is using the same application to get new access tokens but with another scope. The v2 version focusses more on RoadTool detection tho, while this detection is more broad. Some organizations have a high BP hit count on Microsoft Azure CLI. To limit those hits, you have three finetune options to enable in the query: - Only alert when first and second login has X time between each other (default 90 minutes if enabled) - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token and request came from a non-compliant device

MITRE ATT&CK Coverage

TacticTechniques
Execution
Credential Access

References

Telemetry Coverage

Rule Body

// TimeDiff threshold in minutes. Needed for some environments with a lot of BP hits on long time frames. Used in scenario where you expect adversary to quickly request new tokens after first token request.
let maxTimeDiff = 90;
// External lookup to get list of FOCI applications
let FociClientApplications = toscalar(externaldata(client_id: string)
    [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true)
    //| project-rename FociClientId = client_id
    | summarize FociClientId = make_list(client_id)
    );
// Get all token requests for Foci clients
let FociTokenRequest = materialize (
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(6h)
    // Filter for sign-ins to home tenant only
    | where HomeTenantId == ResourceTenantId
    // Lookup for FOCI client
    | where AppId in (FociClientApplications)
    );
FociTokenRequest
// First get all initial logins without refresh tokens as incomming token type
| where IncomingTokenType == "none"
// Then get logins with refresh tokens for same session
| join kind=inner (
    FociTokenRequest
    | where IncomingTokenType != "none"
    | project-rename
        SecondAppDisplayName = AppDisplayName,
        SecondRequestTimeGenerated = TimeGenerated,
        SecondAppId = AppId
    )
    on SessionId, UserPrincipalName
// Exclude when First App ID and Second are the same
| where AppDisplayName != SecondAppDisplayName
// Only get requests where refresh token was used after first sign-in
| extend TimeDiff = datetime_diff('minute', SecondRequestTimeGenerated, TimeGenerated)
| where TimeDiff >= 0 //and TimeDiff <= maxTimeDiff // Remove from comment you want to apply time difference restriction
// Only project needed columns
| project
    FirstRequestTimeGenerated = TimeGenerated,
    FirstResult = ResultType,
    FirstResultDescription = ResultDescription,
    Identity,
    Location,
    FirstAppDisplayName = AppDisplayName,
    FirstAppId = AppId,
    ClientAppUsed,
    DeviceDetail,
    SecondDeviceDetail = DeviceDetail1,
    IPAddress,
    LocationDetails,
    UserAgent,
    SecondRequestTimeGenerated,
    SecondResult = ResultType,
    SecondResultDescription = ResultDescription1,
    SecondAppDisplayName,
    SecondAppId,
    SeconIncomingTokenType = IncomingTokenType1,
    SessionId,
    TimeDiff,
    AuthenticationProcessingDetails,
    SecondAuthenticationProcessingDetails = AuthenticationProcessingDetails1
// Flag logins to the following applications as second login (since they are very popular for attackers and we rather not see logins to these via foci tokens)
| where SecondAppDisplayName in ("Microsoft Azure CLI", "Microsoft Azure PowerShell", "Office 365 Management")
// ENVIRONMENT SPECIFIC FINETUNING - BEGIN
// Most BP triggers are mainly on Microsoft Azure CLI, so we provide two ways of handling these BP detections (strongly depends on environment)
// OPTION 1 - Flag login to Azure CLI using 'Global Administrator' ID in token scope
//| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10"))
// OPTION 2 - Flag login to Azure CLI using 'Global Administrator' ID in token scope from non compliant device
//| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10" and todynamic(SecondDeviceDetail).isCompliant != "true"))
// ENVIRONMENT SPECIFIC FINETUNING - END

Stages and Predicates

Parameters

let maxTimeDiff = 90;

let FociTokenRequest is inlined into the numbered stages below.

Let Binding: FociClientApplications used in Stage 4

let FociClientApplications = toscalar(externaldata(client_id: string)
    [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true)
    | summarize FociClientId = make_list(client_id)
    );

Stages 1 to 4 define let FociTokenRequest as the rule's main pipeline source. Stages 5 to 11 use it.

Stage 1: source

AADNonInteractiveUserSignInLogs

Stage 2: where

| where TimeGenerated > ago(6h)

Stage 3: where

| where HomeTenantId == ResourceTenantId

Stage 4: where

| where AppId in (FociClientApplications)

Stage 5: where

FociTokenRequest
| where IncomingTokenType == "none"

Stage 6: join

| join kind=inner (
    FociTokenRequest
    | where IncomingTokenType != "none"
    | project-rename
        SecondAppDisplayName = AppDisplayName,
        SecondRequestTimeGenerated = TimeGenerated,
        SecondAppId = AppId
    )
    on SessionId, UserPrincipalName

Stage 7: where

where AppDisplayName != SecondAppDisplayName

Stage 8: extend

extend TimeDiff

Stage 9: where

where TimeDiff >= 0

Stage 10: project

project AuthenticationProcessingDetails, ClientAppUsed, DeviceDetail, FirstAppDisplayName, FirstAppId, FirstRequestTimeGenerated, FirstResult, FirstResultDescription, IPAddress, Identity, Location, LocationDetails, SeconIncomingTokenType, SecondAppDisplayName, SecondAppId, SecondAuthenticationProcessingDetails, SecondDeviceDetail, SecondRequestTimeGenerated, SecondResult, SecondResultDescription, SessionId, TimeDiff, UserAgent

Stage 11: where

where SecondAppDisplayName in ("Microsoft Azure CLI", "Microsoft Azure PowerShell", "Office 365 Management")

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthenticationProcessingDetailsproject
ClientAppUsedproject
DeviceDetailproject
FirstAppDisplayNameproject
FirstAppIdproject
FirstRequestTimeGeneratedproject
FirstResultproject
FirstResultDescriptionproject
IPAddressproject
Identityproject
Locationproject
LocationDetailsproject
SeconIncomingTokenTypeproject
SecondAppDisplayNameproject
SecondAppIdproject
SecondAuthenticationProcessingDetailsproject
SecondDeviceDetailproject
SecondRequestTimeGeneratedproject
SecondResultproject
SecondResultDescriptionproject
SessionIdproject
TimeDiffproject
UserAgentproject

Egress Defend - Dangerous Link Click

#
Status
available
Severity
medium
Time window
30m
Source
github.com/Azure/Azure-Sentinel

'Defend has detected a user has clicked a dangerous link in their mailbox.'

MITRE ATT&CK Coverage

TacticTechniques
Execution
Execution

Rule Body

id: a896123e-03a5-4a4d-a7e3-fd814846dfb2
name: Egress Defend - Dangerous Link Click
description: |
  'Defend has detected a user has clicked a dangerous link in their mailbox.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: EgressDefend
    dataTypes:
      - EgressDefend_CL
      - EgressDefend_v4_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
  - T0853
tags:
  - Defend
query: |
  DefendAuditData_v4
  | where LinksClicked > 0
  | where ThreatLevel == "dangerous" or ThreatLevel == "suspicious"
  | extend Account_0_FullName = trim(@"[^@.\w]+",Recipients)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account_0_FullName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SenderIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: Account_0_FullName
customDetails:
  DefendSenderIP: SenderIP
  DefendSender: From
  timesClicked: LinksClicked
alertDetailsOverride:
  alertDisplayNameFormat: Alert - {{Account_0_FullName}} as clicked a suspicious link.
version: 1.1.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DefendAuditData_v4

Stage 2: where

| where LinksClicked > 0

Stage 3: where

| where ThreatLevel == "dangerous" or ThreatLevel == "suspicious"

Stage 4: extend

| extend Account_0_FullName = trim(@"[^@.\w]+",Recipients)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LinksClickedgt
  • 0 transforms: number
field:"LinksClicked" kind:gt value:"0"
ThreatLeveleq
  • dangerous
  • suspicious
field:"ThreatLevel" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
Account_0_FullNameextend

GCP Audit Logs - Detect Organization Policy Deletion or Updation

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

Detects when a Google Cloud Platform organization policy is deleted or updated. Organization policies provide centralized control over your organization's cloud resources and help ensure security and compliance. Deletion or modification of org policies may indicate an attempt to bypass security controls or weaken the security posture of GCP projects. Adversaries may delete or update organization policies to disable security constraints before performing malicious activities.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 205e1c9f-faee-43f1-b3b8-1952ffbbeea4
name: GCP Audit Logs - Detect Organization Policy Deletion or Updation
description: |
  'Detects when a Google Cloud Platform organization policy is deleted or updated. 
  Organization policies provide centralized control over your organization's cloud resources and help ensure security and compliance.
  Deletion or modification of org policies may indicate an attempt to bypass security controls or weaken the security posture of GCP projects.
  Adversaries may delete or update organization policies to disable security constraints before performing malicious activities.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.001
tags:
  - GCP
  - IAM Organization Policy
  - Compliance
query: |
  GCPAuditLogs
  | where ServiceName == "orgpolicy.googleapis.com"
  | where MethodName has_any ("OrgPolicy.DeletePolicy", "OrgPolicy.UpdatePolicy")
  | extend 
      RequestMetadataJson = parse_json(RequestMetadata),
      AuthInfoJson = parse_json(AuthenticationInfo),
      AuthzInfoJson = parse_json(AuthorizationInfo)
  | extend 
      PolicyName = split(GCPResourceName, "/")[-1],
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
      AuthEmail = tostring(AuthInfoJson.principalEmail),
      Permission = tostring(AuthzInfoJson[0].permission),
      PermissionGranted = tostring(AuthzInfoJson[0].granted)
  | extend 
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project TimeGenerated,
            PrincipalEmail,
            AuthEmail,
            ProjectId,
            ResourceName = GCPResourceName,
            PolicyName,
            CallerIpAddress,
            UserAgent,
            MethodName,
            ServiceName,
            Severity,
            Permission,
            PermissionGranted,
            LogName,
            InsertId,
            AccountName,
            AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
      - identifier: InstanceName
        columnName: ResourceName
customDetails:
  ProjectId: ProjectId
  PolicyName: PolicyName
  ResourceName: ResourceName
  UserAgent: UserAgent
  Permission: Permission
  MethodName: MethodName
alertDetailsOverride:
  alertDisplayNameFormat: "GCP Organization Policy {{PolicyName}} Deleted by {{PrincipalEmail}}"
  alertDescriptionFormat: |-
    Organization policy {{PolicyName}} was deleted. This action may weaken security controls and compliance posture.
    
    Resource: {{ResourceName}}
    Source IP: {{CallerIpAddress}}
    
    Investigate whether this deletion was authorized and assess the impact on security controls.
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPAuditLogs

Stage 2: where

| where ServiceName == "orgpolicy.googleapis.com"

Stage 3: where

| where MethodName has_any ("OrgPolicy.DeletePolicy", "OrgPolicy.UpdatePolicy")

Stage 4: extend (3 consecutive steps)

| extend 
    RequestMetadataJson = parse_json(RequestMetadata),
    AuthInfoJson = parse_json(AuthenticationInfo),
    AuthzInfoJson = parse_json(AuthorizationInfo)
| extend 
    PolicyName = split(GCPResourceName, "/")[-1],
    CallerIpAddress = tostring(RequestMetadataJson.callerIp),
    UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
    AuthEmail = tostring(AuthInfoJson.principalEmail),
    Permission = tostring(AuthzInfoJson[0].permission),
    PermissionGranted = tostring(AuthzInfoJson[0].granted)
| extend 
    AccountName = tostring(split(PrincipalEmail, "@")[0]), 
    AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])

Stage 5: project

| project TimeGenerated,
          PrincipalEmail,
          AuthEmail,
          ProjectId,
          ResourceName = GCPResourceName,
          PolicyName,
          CallerIpAddress,
          UserAgent,
          MethodName,
          ServiceName,
          Severity,
          Permission,
          PermissionGranted,
          LogName,
          InsertId,
          AccountName,
          AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MethodNamematch
  • OrgPolicy.DeletePolicy transforms: term
  • OrgPolicy.UpdatePolicy transforms: term
field:"gcp::method_name" kind:match
ServiceNameeq
  • orgpolicy.googleapis.com
field:"ServiceName" kind:eq value:"orgpolicy.googleapis.com"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
AuthEmailproject
CallerIpAddressproject
InsertIdproject
LogNameproject
MethodNameproject
Permissionproject
PermissionGrantedproject
PolicyNameproject
PrincipalEmailproject
ProjectIdproject
ResourceNameproject
ServiceNameproject
Severityproject
TimeGeneratedproject
UserAgentproject

GCP Audit Logs - Open Firewall Rule Created or Modified

#
Status
available
Severity
high
Time window
1h
Group by
AuthEmail, CallerIpAddress, Direction, FirewallRuleName, GCPResourceName, InsertId, LogName, MethodName, Network, OperationType, PrincipalEmail, Priority, ProjectId, ServiceName, Severity, TimeGenerated, UserAgent
Source
github.com/Azure/Azure-Sentinel

Detects when a Google Cloud Platform firewall rule is created or modified to allow traffic from any source (0.0.0.0/0 or 0.0.0.0). Open firewall rules expose resources to the internet and can significantly increase the attack surface of cloud infrastructure. This may indicate a misconfiguration, lack of security awareness, or malicious activity to create backdoor access. Adversaries may create or modify firewall rules to enable persistent access or facilitate lateral movement. This rule monitors firewall insert and patch operations where sourceRanges include unrestricted access patterns.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 8061c611-55f1-4ee5-a8f8-8f19f2c7aab2
name: GCP Audit Logs - Open Firewall Rule Created or Modified
description: |
  'Detects when a Google Cloud Platform firewall rule is created or modified to allow traffic from any source (0.0.0.0/0 or 0.0.0.0).
  Open firewall rules expose resources to the internet and can significantly increase the attack surface of cloud infrastructure.
  This may indicate a misconfiguration, lack of security awareness, or malicious activity to create backdoor access.
  Adversaries may create or modify firewall rules to enable persistent access or facilitate lateral movement.
  This rule monitors firewall insert and patch operations where sourceRanges include unrestricted access patterns.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1562.004
  - T1133
  - T1562.001
tags:
  - GCP
  - Firewall
  - Network Security
  - Misconfiguration
query: |
  GCPAuditLogs
  | where ServiceName == "compute.googleapis.com"
  | where MethodName has_any ("firewalls.insert", "firewalls.patch")
  | where GCPResourceType == "gce_firewall_rule"
  | where Severity == "NOTICE"
  | extend 
      RequestJson = parse_json(Request),
      RequestMetadataJson = parse_json(RequestMetadata),
      AuthInfoJson = parse_json(AuthenticationInfo)
  | extend 
      SourceRanges = RequestJson.sourceRanges,
      Alloweds = RequestJson.alloweds,
      Direction = tostring(RequestJson.direction),
      RuleName = tostring(RequestJson.name),
      Network = tostring(RequestJson.network),
      Priority = tostring(RequestJson.priority),
      LogConfig = RequestJson.logConfig,
      Disabled = tobool(RequestJson.disabled)
  | mv-expand SourceRange = SourceRanges
  | extend SourceRangeStr = tostring(SourceRange)
  | where SourceRangeStr in ("0.0.0.0/0", "0.0.0.0")
  | extend 
      FirewallRuleName = extract(@"firewalls/([^/]+)$", 1, GCPResourceName),
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
      AuthEmail = tostring(AuthInfoJson.principalEmail)
  | mv-expand Allowed = Alloweds
  | extend 
      AllowedProtocol = tostring(Allowed.IPProtocol),
      AllowedPorts = tostring(Allowed.ports),
      OperationType = case(
          MethodName has "insert", "Created",
          MethodName has "patch", "Modified",
          "Unknown")
  | summarize 
      AllowedProtocols = make_set(AllowedProtocol, 30),
      AllowedPortsList = make_set(AllowedPorts, 100),
      SourceRangesList = make_set(SourceRangeStr, 100)
      by TimeGenerated, PrincipalEmail, AuthEmail, ProjectId, FirewallRuleName, 
         GCPResourceName, Direction, Priority, Network, CallerIpAddress, UserAgent, 
         MethodName, ServiceName, Severity, OperationType, LogName, InsertId
  | extend
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project TimeGenerated,
            PrincipalEmail,
            AuthEmail,
            ProjectId,
            FirewallRuleName,
            ResourceName = GCPResourceName,
            OperationType,
            Direction,
            SourceRangesList,
            AllowedProtocols,
            AllowedPortsList,
            Priority,
            Network,
            CallerIpAddress,
            UserAgent,
            MethodName,
            ServiceName,
            Severity,
            LogName,
            InsertId,
            AccountName,
            AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
      - identifier: InstanceName
        columnName: ResourceName
customDetails:
  ProjectId: ProjectId
  FirewallRuleName: FirewallRuleName
  ResourceName: ResourceName
  OperationType: OperationType
  Direction: Direction
  SourceRanges: SourceRangesList
  AllowedProtocols: AllowedProtocols
  AllowedPorts: AllowedPortsList
  Network: Network
alertDetailsOverride:
  alertDisplayNameFormat: "GCP Open Firewall Rule {{OperationType}} - {{FirewallRuleName}} by {{PrincipalEmail}}"
  alertDescriptionFormat: |-
    User {{PrincipalEmail}} created/modified firewall rule {{FirewallRuleName}} in project {{ProjectId}} to allow traffic from unrestricted sources (0.0.0.0/0).
    This configuration exposes resources to the internet and significantly increases attack surface. Investigate immediately to determine if this was authorized and assess potential security exposure. Consider restricting source ranges to specific trusted IP addresses or networks.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPAuditLogs

Stage 2: where

| where ServiceName == "compute.googleapis.com"

Stage 3: where

| where MethodName has_any ("firewalls.insert", "firewalls.patch")

Stage 4: where

| where GCPResourceType == "gce_firewall_rule"

Stage 5: where

| where Severity == "NOTICE"

Stage 6: extend

| extend 
    RequestJson = parse_json(Request),
    RequestMetadataJson = parse_json(RequestMetadata),
    AuthInfoJson = parse_json(AuthenticationInfo)

Stage 7: extend

| extend 
    SourceRanges = RequestJson.sourceRanges,
    Alloweds = RequestJson.alloweds,
    Direction = tostring(RequestJson.direction),
    RuleName = tostring(RequestJson.name),
    Network = tostring(RequestJson.network),
    Priority = tostring(RequestJson.priority),
    LogConfig = RequestJson.logConfig,
    Disabled = tobool(RequestJson.disabled)

Stage 8: mv-expand

| mv-expand SourceRange = SourceRanges

Stage 9: extend

| extend SourceRangeStr = tostring(SourceRange)

Stage 10: where

| where SourceRangeStr in ("0.0.0.0/0", "0.0.0.0")

Stage 11: extend

| extend 
    FirewallRuleName = extract(@"firewalls/([^/]+)$", 1, GCPResourceName),
    CallerIpAddress = tostring(RequestMetadataJson.callerIp),
    UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
    AuthEmail = tostring(AuthInfoJson.principalEmail)

Stage 12: mv-expand

| mv-expand Allowed = Alloweds

Stage 13: extend

| extend 
    AllowedProtocol = tostring(Allowed.IPProtocol),
    AllowedPorts = tostring(Allowed.ports),
    OperationType = case(
        MethodName has "insert", "Created",
        MethodName has "patch", "Modified",
        "Unknown")
OperationType =
ifMethodName has "insert""Created"
elifMethodName has "patch""Modified"
else"Unknown"

Stage 14: summarize

| summarize 
    AllowedProtocols = make_set(AllowedProtocol, 30),
    AllowedPortsList = make_set(AllowedPorts, 100),
    SourceRangesList = make_set(SourceRangeStr, 100)
    by TimeGenerated, PrincipalEmail, AuthEmail, ProjectId, FirewallRuleName, 
       GCPResourceName, Direction, Priority, Network, CallerIpAddress, UserAgent, 
       MethodName, ServiceName, Severity, OperationType, LogName, InsertId

Stage 15: extend

| extend
    AccountName = tostring(split(PrincipalEmail, "@")[0]), 
    AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])

Stage 16: project

| project TimeGenerated,
          PrincipalEmail,
          AuthEmail,
          ProjectId,
          FirewallRuleName,
          ResourceName = GCPResourceName,
          OperationType,
          Direction,
          SourceRangesList,
          AllowedProtocols,
          AllowedPortsList,
          Priority,
          Network,
          CallerIpAddress,
          UserAgent,
          MethodName,
          ServiceName,
          Severity,
          LogName,
          InsertId,
          AccountName,
          AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
GCPResourceTypeeq
  • gce_firewall_rule
field:"gcp::resource_type" kind:eq value:"gce_firewall_rule"
MethodNamematch
  • firewalls.insert transforms: term
  • firewalls.patch transforms: term
field:"gcp::method_name" kind:match
ServiceNameeq
  • compute.googleapis.com corpus 2 (kusto 2)
field:"ServiceName" kind:eq value:"compute.googleapis.com"
Severityeq
  • NOTICE corpus 4 (kusto 4)
field:"Severity" kind:eq value:"NOTICE"
SourceRangeStrin
  • 0.0.0.0
  • 0.0.0.0/0
field:"SourceRangeStr" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
AllowedPortsListproject
AllowedProtocolsproject
AuthEmailproject
CallerIpAddressproject
Directionproject
FirewallRuleNameproject
InsertIdproject
LogNameproject
MethodNameproject
Networkproject
OperationTypeproject
PrincipalEmailproject
Priorityproject
ProjectIdproject
ResourceNameproject
ServiceNameproject
Severityproject
SourceRangesListproject
TimeGeneratedproject
UserAgentproject

Google DNS - Exchange online autodiscover abuse

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

'Detects possible Exchange online autodiscover abuse.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Rule Body

id: 424c2aca-5367-4247-917a-5d0f7035e40e
name: Google DNS - Exchange online autodiscover abuse
description: |
  'Detects possible Exchange online autodiscover abuse.'
severity: Medium
requiredDataConnectors:
  - connectorId: GCPDNSDataConnector
    dataTypes:
      - GCPCloudDNS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1566
  - T1187
query: |
  GCPCloudDNS
  | where Query has_any ('autodiscover.com.br', 'autodiscover.com.cn', 'autodiscover.com.co', 'autodiscover.es', 'autodiscover.fr', 'autodiscover.in', 'autodiscover.it', 'autodiscover.sg', 'autodiscover.uk', 'autodiscover.xyz', 'autodiscover.online')
  | extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DNSCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPCloudDNS

Stage 2: where

| where Query has_any ('autodiscover.com.br', 'autodiscover.com.cn', 'autodiscover.com.co', 'autodiscover.es', 'autodiscover.fr', 'autodiscover.in', 'autodiscover.it', 'autodiscover.sg', 'autodiscover.uk', 'autodiscover.xyz', 'autodiscover.online')

Stage 3: extend

| extend DNSCustomEntity = Query, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Querymatch
  • autodiscover.com.br transforms: term
  • autodiscover.com.cn transforms: term
  • autodiscover.com.co transforms: term
  • autodiscover.es transforms: term
  • autodiscover.fr transforms: term
  • autodiscover.in transforms: term
  • autodiscover.it transforms: term
  • autodiscover.online transforms: term
  • autodiscover.sg transforms: term
  • autodiscover.uk transforms: term
  • autodiscover.xyz transforms: term
field:"Query" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DNSCustomEntityextend
IPCustomEntityextend

Guardian- Content Access Control Allowed List Policy Violation Detection

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

'This alert creates an incident when Content Access Control Allowed List Policy Violation detected from the Guardian.'

Rule Body

id: aa88a190-d4d8-4f32-b533-3aa097c9ce3d
name: Guardian- Content Access Control Allowed List Policy Violation Detection
description: |
  'This alert creates an incident when Content Access Control Allowed List Policy Violation detected from the Guardian.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Allowed List'
  | where Severity =~ 'Low'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Content Access Control Allowed List Policy Violation Detection
  alertDescriptionFormat: |
    This query detects Allowed List Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Allowed List'

Stage 3: where

| where Severity =~ 'Low'

Indicators

These rows show field, operator, and value matches.

McAfee ePO - Agent Handler down

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

Detects when AgentHandler is down.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 3c1425d3-93d4-4eaf-8aa0-370dbac94c82
name: McAfee ePO - Agent Handler down
description: |
  'Detects when AgentHandler is down.'
severity: Medium
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  McAfeeEPOEvent
  | where EventId == '16025'
  | extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where EventId == '16025'

Stage 3: extend

| extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
IPCustomEntityextend

Network Port Sweep from External Network (ASIM Network Session schema)

#
Status
available
Severity
high
Time window
1h
Group by
DstPortNumber, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

This detection rule detects scenarios when a particular port is being scanned by multiple external sources. The rule utilize ASIM normalization, and is applied to any source which supports the ASIM Network Session schema.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: cd8faa84-4464-4b4e-96dc-b22f50c27541
name: Network Port Sweep from External Network (ASIM Network Session schema)
description: |
  'This detection rule detects scenarios when a particular port is being scanned by multiple external sources. The rule utilize [ASIM](https://aka.ms/AboutASIM) normalization, and is applied to any source which supports the ASIM Network Session schema.'
severity: High
status: Available
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors:
  - connectorId: AWSS3
    dataTypes:
      - AWSVPCFlow
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftSysmonForLinux
    dataTypes:
      - Syslog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
  - connectorId: AzureNSG
    dataTypes:
      - AzureDiagnostics
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: Corelight
    dataTypes:
      - Corelight_CL
  - connectorId: AIVectraStream
    dataTypes:
      - VectraStream
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoMeraki
    dataTypes:
      - Syslog
      - CiscoMerakiNativePoller

queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
  - Discovery
relevantTechniques:
  - T1590
  - T1046
query: |
  let lookback = 1h;
  let threshold = 20;
  _Im_NetworkSession(starttime=ago(lookback),endtime=now())
  | where NetworkDirection == "Inbound"
  | summarize make_set(DstIpAddr,100) by SrcIpAddr, DstPortNumber
  | where array_length(set_DstIpAddr) > threshold
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails:
  AllDstIpAddr: set_DstIpAddr

alertDetailsOverride:
  alertDisplayNameFormat: Network Port Sweep detected on {{DstPortNumber}}
  alertDescriptionFormat: 'Network Port Sweep was detection by multiple IPs'
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1h;
let threshold = 20;

Stage 1: source

_Im_NetworkSession(starttime=ago(lookback),endtime=now())

Stage 2: where

| where NetworkDirection == "Inbound"

Stage 3: summarize

| summarize make_set(DstIpAddr,100) by SrcIpAddr, DstPortNumber

Stage 4: where

| where array_length(set_DstIpAddr) > threshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
NetworkDirectioneq
  • Inbound corpus 2 (kusto 2)
field:"NetworkDirection" kind:eq value:"Inbound"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstPortNumbersummarize
SrcIpAddrsummarize

SAP ETD - SAP system stopped reporting data

#

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
Time window
7d
Group by
SystemId
Source
github.com/Azure/Azure-Sentinel

Identifies a per-system silence when an individual SAP system (identified by its SID) that has recently been reporting to SAP Enterprise Threat Detection (ETD) stops producing new records in the SAPETDAlerts_CL table within the configured per-system grace period (default 2 hours). A targeted silence of a single SID may indicate that an adversary with access to the SAP system, the SAP ETD collector for that SID, or the data connector is selectively blocking security telemetry to hide follow-on activity while leaving the rest of the SAP ETD feed intact; benign causes such as connectivity issues, collector misconfiguration, or planned maintenance for that SID are also possible and should be ruled out during triage. The set of "expected" SIDs is derived from any system that has reported within the BaselineLookback period (default 7 days); systems silent for longer are considered decommissioned and are not alerted on. Tunable parameters at the top of the query: LookbackPeriod (silence threshold per SID; align with queryFrequency) and BaselineLookback (how far back to look to discover known SIDs; align with queryPeriod). This rule is complementary to the overall-feed rule "SAP ETD - No new data received".

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: b1413b43-9410-46f4-94d9-da507105d834
kind: Scheduled
name: SAP ETD - SAP system stopped reporting data
description: |
  Identifies a per-system silence when an individual SAP system (identified by its SID) that has recently been reporting to SAP Enterprise Threat Detection (ETD) stops producing new records in the SAPETDAlerts_CL table within the configured per-system grace period (default 2 hours). A targeted silence of a single SID may indicate that an adversary with access to the SAP system, the SAP ETD collector for that SID, or the data connector is selectively blocking security telemetry to hide follow-on activity while leaving the rest of the SAP ETD feed intact; benign causes such as connectivity issues, collector misconfiguration, or planned maintenance for that SID are also possible and should be ruled out during triage. The set of "expected" SIDs is derived from any system that has reported within the `BaselineLookback` period (default 7 days); systems silent for longer are considered decommissioned and are not alerted on. Tunable parameters at the top of the query: `LookbackPeriod` (silence threshold per SID; align with `queryFrequency`) and `BaselineLookback` (how far back to look to discover known SIDs; align with `queryPeriod`). This rule is complementary to the overall-feed rule "SAP ETD - No new data received".
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SAPETDAlerts
    dataTypes:
      - SAPETDAlerts_CL
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1562.006
query: |
  // ---- Configurable thresholds ----
  let LookbackPeriod = 2h;
  let BaselineLookback = 7d;
  // ---------------------------------
  let regex_sid = @"^([A-Z0-9]{3})/";
  let regex_client = @'\/(.{3})$';
  SAPETDAlerts_CL
  | where TimeGenerated > ago(BaselineLookback)
  | mv-expand NormalizedTriggeringEvents
  | extend
      SystemId = extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
      ClientId = extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
      Host     = tostring(NormalizedTriggeringEvents.NetworkHostnameInitiator),
      Instance = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)
  | where isnotempty(SystemId)
  | summarize
      LastIngestionTime = max(TimeGenerated),
      Host     = take_any(Host),
      Instance = take_any(Instance),
      ClientId = take_any(ClientId)
      by SystemId
  | extend TimeSinceLastIngestion = now() - LastIngestionTime
  | where TimeSinceLastIngestion > LookbackPeriod
  | extend
      LookbackPeriod = LookbackPeriod,
      Reason = strcat("SAP system ", SystemId, " has not reported any data to SAP ETD in the last ", tostring(LookbackPeriod), " (last seen: ", tostring(LastIngestionTime), ").")
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: SystemId
      - identifier: AppId
        columnName: ClientId
      - identifier: InstanceName
        columnName: Instance
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Host
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP ETD - SAP system {{SystemId}} stopped reporting data'
  alertDescriptionFormat: |
    {{Reason}}

    A selective silence of a single SAP SID may indicate that an adversary is tampering with the security telemetry pipeline for this specific system (for example by stopping the SAP ETD collector for that SID, disabling the relevant data connector path, or blocking network egress from that host) in order to hide malicious activity while leaving the rest of the SAP ETD feed intact. Treat the silence as suspicious until proven otherwise: validate the integrity and runtime state of the SAP system, the SAP ETD collector configuration for this SID, and the data connector between SAP ETD and Microsoft Sentinel, and review recent change / admin activity on those components before concluding the cause is a benign outage.
customDetails:
  SAP_SID: SystemId
  SAP_Client: ClientId
  LookbackPeriod: LookbackPeriod
  LastIngestion: LastIngestionTime
  LastIngestionGap: TimeSinceLastIngestion
version: 1.0.0

Stages and Predicates

Parameters

let LookbackPeriod = 2h;
let BaselineLookback = 7d;
let regex_sid = @"^([A-Z0-9]{3})/";
let regex_client = @'\/(.{3})$';

Stage 1: source

SAPETDAlerts_CL

Stage 2: where

| where TimeGenerated > ago(BaselineLookback)

Stage 3: mv-expand

| mv-expand NormalizedTriggeringEvents

Stage 4: extend

| extend
    SystemId = extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
    ClientId = extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
    Host     = tostring(NormalizedTriggeringEvents.NetworkHostnameInitiator),
    Instance = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)

Stage 5: where

| where isnotempty(SystemId)

Stage 6: summarize

| summarize
    LastIngestionTime = max(TimeGenerated),
    Host     = take_any(Host),
    Instance = take_any(Instance),
    ClientId = take_any(ClientId)
    by SystemId

Stage 7: extend

| extend TimeSinceLastIngestion = now() - LastIngestionTime

Stage 8: where

| where TimeSinceLastIngestion > LookbackPeriod

Stage 9: extend

| extend
    LookbackPeriod = LookbackPeriod,
    Reason = strcat("SAP system ", SystemId, " has not reported any data to SAP ETD in the last ", tostring(LookbackPeriod), " (last seen: ", tostring(LastIngestionTime), ").")

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIdsummarize
Hostsummarize
Instancesummarize
LastIngestionTimesummarize
SystemIdsummarize
TimeSinceLastIngestionextend
LookbackPeriodextend
Reasonextend

Sonrai Ticket Closed

#

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
low
Time window
5m
Source
github.com/Azure/Azure-Sentinel

'Checks if Sonrai tickets have been closed. It uses the action type to check if a ticket has been closed'

MITRE ATT&CK Coverage

Rule Body

id: f5d467de-b5a2-4b4f-96db-55e27c733594
name: Sonrai Ticket Closed
description: |
  'Checks if Sonrai tickets have been closed. 
  It uses the action type to check if a ticket has been closed'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: SonraiDataConnector
    dataTypes:
      - Sonrai_Tickets_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Execution
  - Exfiltration
  - Impact
  - InitialAccess
  - LateralMovement
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1566
  - T1059
  - T1547
  - T1548
  - T1562
  - T1003
  - T1087
  - T1021
  - T1119
  - T1071
  - T1041
  - T1499
query: |
  Sonrai_Tickets_CL
  | where action_d == 2
entityMappings: 
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: digest_criticalResourceName_s
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  ticketName: digest_title_s
  ticketStatus: digest_status_s
  ticketSeverity: digest_severityCategory_s
  resourceLabel: digest_resourceLabel_s
  resourceType: digest_resourceType_s
  criticalResource: digest_criticalResourceName_s
  ticketOrg: digest_org_s
alertDetailsOverride:
  alertSeverityColumnName: digest_severityCategory_s
  alertDisplayNameFormat: Closed - {{digest_ticketSrn_s}} - {{digest_ticketKeyName_s}}
  alertDescriptionFormat: digest_ticketKeyDescription_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

Sonrai_Tickets_CL

Stage 2: where

| where action_d == 2

Indicators

These rows show field, operator, and value matches.

Stale IAAS policy attachment to role

#

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
informational
Time window
30m
Source
github.com/Azure/Azure-Sentinel

The rule detects 'IaaS policies' attached to a role that has not used them during the past X days. It is recommended to remove unused policies from identities to reduce risk.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Privilege Escalation

Rule Body

id: ccdf3f87-7890-4549-9d0f-8f43c1d2751d
name: Stale IAAS policy attachment to role
kind: Scheduled
description: The rule detects 'IaaS policies' attached to a role that has not used them during the past X days. It is recommended to remove unused policies from identities to reduce risk.
severity: Informational
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1098
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s
  | where Policy has "Stale IAAS policy attachment to role"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - Stale IAAS policy attachment to role
  alertDescriptionFormat: Stale IAAS policy attachment to role. The rule detects 'IaaS policies' attached to a role that has not used them during the past X days.It is recommended to remove unused policies from identities to reduce risk.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.3

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "Stale IAAS policy attachment to role"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • Stale IAAS policy attachment to role transforms: term
field:"policy_name_s" kind:match value:"Stale IAAS policy attachment to role"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Suspicious Powershell Commandlet Executed

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

This analytic rule detects when a suspicious PowerShell commandlet is executed on a host. Threat actors often use PowerShell to execute commands and scripts to move laterally, escalate privileges, and exfiltrate data.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: b5153fb3-ada9-4ce4-9131-79c771efb50d
name: Suspicious Powershell Commandlet Executed 
description: |
  This analytic rule detects when a suspicious PowerShell commandlet is executed on a host. Threat actors often use PowerShell to execute commands and scripts to move laterally, escalate privileges, and exfiltrate data.
severity: Medium
status: Available 
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  // Adjust the list of suspicious commandlets as needed
  let SuspiciousPowerShellCommandList = dynamic(["Get-ADUserResultantPasswordPolicy",
    "Get-DomainPolicy",
    "Get-DomainUser",
    "Get-DomainComputer",
    "Get-DomainController",
    "Get-DomainGroup",
    "Get-DomainTrust",
    "Get-ADTrust",
    "Get-ForestTrust"
    ]);
  DeviceEvents
  | where ActionType == "PowerShellCommand"
  | extend Commandlet = tostring(parse_json(AdditionalFields).Command)
  | where Commandlet has_any (SuspiciousPowerShellCommandList)
  | project TimeGenerated, DeviceName, LocalIP, InitiatingProcessAccountUpn, InitiatingProcessId, InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine
  | extend Username = tostring(split(InitiatingProcessAccountUpn, '@')[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@')[1])
  | extend DvcHostname = tostring(split(DeviceName, '.')[0]), DvcDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: DvcHostname
      - identifier: DnsDomain
        columnName: DvcDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: LocalIP
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Username
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: InitiatingProcessId
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Suspicious PowerShell Commandlet Executed on {{DvcHostname}} ({{LocalIP}}) by ({{InitiatingProcessAccountUpn}})"
  alertDescriptionFormat: "Suspicious PowerShell Commandlet by Process '{{InitiatingProcessFileName}}' ProcessId: '{{InitiatingProcessId}}' with commandline {{InitiatingProcessCommandLine}} was executed."
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SuspiciousPowerShellCommandList used in Stage 4

let SuspiciousPowerShellCommandList = dynamic(["Get-ADUserResultantPasswordPolicy",
  "Get-DomainPolicy",
  "Get-DomainUser",
  "Get-DomainComputer",
  "Get-DomainController",
  "Get-DomainGroup",
  "Get-DomainTrust",
  "Get-ADTrust",
  "Get-ForestTrust"
  ]);

Stage 1: source

DeviceEvents

Stage 2: where

| where ActionType == "PowerShellCommand"

Stage 3: extend

| extend Commandlet = tostring(parse_json(AdditionalFields).Command)

Stage 4: where

| where Commandlet has_any (SuspiciousPowerShellCommandList)

Stage 5: project

| project TimeGenerated, DeviceName, LocalIP, InitiatingProcessAccountUpn, InitiatingProcessId, InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine

Stage 6: extend

| extend Username = tostring(split(InitiatingProcessAccountUpn, '@')[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@')[1])

Stage 7: extend

| extend DvcHostname = tostring(split(DeviceName, '.')[0]), DvcDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • PowerShellCommand
field:"ActionType" kind:eq value:"PowerShellCommand"
Commandletmatch
  • Get-ADTrust transforms: term
  • Get-ADUserResultantPasswordPolicy transforms: term
  • Get-DomainComputer transforms: term
  • Get-DomainController transforms: term
  • Get-DomainGroup transforms: term
  • Get-DomainPolicy transforms: term
  • Get-DomainTrust transforms: term
  • Get-DomainUser transforms: term
  • Get-ForestTrust transforms: term
field:"Commandlet" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNameproject
InitiatingProcessAccountUpnproject
InitiatingProcessCommandLineproject
InitiatingProcessFileNameproject
InitiatingProcessFolderPathproject
InitiatingProcessIdproject
LocalIPproject
TimeGeneratedproject
UPNSuffixextend
Usernameextend
DvcDomainextend
DvcHostnameextend

Theom Critical Risks

#

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
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"Creates Microsoft Sentinel incidents for critical risk Theom alerts."

MITRE ATT&CK Coverage

Rule Body

id: bb9051ef-0e72-4758-a143-80c25ee452f0
name: Theom Critical Risks
description: |
  "Creates Microsoft Sentinel incidents for critical risk Theom alerts."
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Exfiltration
  - Impact
  - Reconnaissance
relevantTechniques:
  - T1592
  - T1589
  - T1070
  - T1552
  - T1619
  - T1119
  - T1560
  - T1530
  - T1213
  - T1001
  - T1041
  - T1537
  - T1485
  - T1486
  - T1565
query: |
  TheomAlerts_CL
   | where priority_s == "P1"
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where priority_s == "P1"

Indicators

These rows show field, operator, and value matches.

Vaults Alerts for Prancer

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

'High severity vault alerts found by Prancer.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: 0b76eef3-5dc0-41b1-9f67-fffa7783f5f6
name: Vaults Alerts for Prancer
description: |
  'High severity vault alerts found by Prancer.'
severity: High
requiredDataConnectors:
- connectorId: PrancerLogData
  dataTypes:
    - prancer_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
query: |
  union prancer_CL
  | where deviceProduct_s == 'azure'
  | where parse_json(data_data_snapshots_s)[0].type == 'Microsoft.KeyVault/vaults'
  | where data_data_severity_s == 'High' and data_data_result_s == 'failed'
  | extend snapshot = parse_json(data_data_snapshots_s)
  | mv-expand snapshot 
  | extend
      id = tostring(snapshot.id),
      structure = tostring(snapshot.structure),
      reference = tostring(snapshot.reference),
      source = tostring(snapshot.source),
      collection = tostring(snapshot.collection),
      type = tostring(snapshot.type),
      region = tostring(snapshot.region),
      resourceTypes = tostring(snapshot.resourceTypes),
      path = tostring(snapshot.path)

customDetails:
entityMappings:
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: path
alertDetailsOverride:
  alertDisplayNameFormat: "{{data_data_message_s}}"  
  alertDescriptionFormat: "{{data_data_description_s}}"
  alertSeverityColumnName: "{{data_data_severity_s}}"
  alertDynamicProperties:
    - alertProperty: RemediationSteps
      value: data_data_remediation_description_s
version: 1.0.2
kind: Scheduled


Stages and Predicates

Stage 1: union

union of 1 branches

Stage 2: source

prancer_CL

Stage 3: where

where deviceProduct_s == "azure"

Stage 4: where

where parse_json(data_data_snapshots_s)[0].type == "Microsoft.KeyVault/vaults"

Stage 5: where

where data_data_result_s == "failed" and data_data_severity_s == "High"

Stage 6: extend

extend snapshot

Stage 7: mv-expand

mv-expand snapshot

Stage 8: extend

extend collection, id, path, reference, region, resourceTypes, source, structure, type

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
data_data_result_seq
  • failed
field:"data_data_result_s" kind:eq value:"failed"
data_data_severity_seq
  • High
field:"data_data_severity_s" kind:eq value:"High"
data_data_snapshots_s[0].typeeq
  • Microsoft.KeyVault/vaults transforms: kusto_accessor_expr:{"expression":"parse_json(data_data_snapshots_s)[0].type","field":"data_data_snapshots_s[0].type"}, kusto_dynamic_path
field:"data_data_snapshots_s[0].type" kind:eq value:"Microsoft.KeyVault/vaults"
deviceProduct_seq
  • azure
field:"deviceProduct_s" kind:eq value:"azure"

Output Fields

These fields are emitted when the rule matches.

FieldSource
snapshotextend
collectionextend
idextend
pathextend
referenceextend
regionextend
resourceTypesextend
sourceextend
structureextend
typeextend

Back to all Kusto rules