Detection rules › Kusto
Kusto rules: 0l
1Password - Privileged vault permission change
#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
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | any: Group vault access (catch-all) |
| 1Password | any: User vault access (catch-all) |
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
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 =isempty(aux_details)aux_uuid""TargetUsername =isnotempty(aux_details)aux_details.email""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | match |
| field:"action" kind:match |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq |
object_uuid | in |
| field:"object_uuid" kind:in value:"watchlist" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
SrcIpAddr | extend |
TargetGroupUUID | extend |
TargetUsername | extend |
[Entra ID] Mass Privileged Role Change Activity Detected
#Detects multiple privileged role changes occurring around the same time. This may signal bulk abuse or administrative misconfiguration.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
- Azure AD Application Administrator Role Assigned (Splunk)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
RoleName | contains |
| field:"RoleName" kind:contains value:"Admin" |
dcount_Target | ge |
| field:"dcount_Target" kind:ge value:"9" |
displayName_ | eq |
| field:"displayName_" kind:eq value:"Role.DisplayName" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Alert_Category_en | project |
Alert_Containment_en | project |
Alert_Description_en | project |
Alert_Name_en | project |
Alert_Remediation_en | project |
Alert_SubCategory_en | project |
Alert_Time_TW | project |
Alert_Time_UTC0 | project |
Alert_TriageStep_en | project |
Event_Action | project |
Event_Status | project |
Source_Identity_FullName | project |
Source_Identity_Type | project |
Source_Network_IPAddress | project |
Source_Network_IPLocation | project |
Target_Identity_DomainType | project |
Target_Identity_FullName | project |
Target_Identity_Type | project |
Target_Resource_ID | project |
Target_Resource_Type | project |
AD user enabled and password not set within 48 hours
#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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq |
EventID | in |
| field:"EventID" kind:in |
PasswordSetAttemptDelta_Min | gt |
| field:"PasswordSetAttemptDelta_Min" kind:gt value:"2880" |
PasswordSetAttemptDelta_Min | is_null | field:"PasswordSetAttemptDelta_Min" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TargetUserName | ends_with | $ | excludes:TargetUserName field:"TargetUserName" value:"$" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity_4722 | project |
Computer_4722 | project |
SubjectAccount_Event4722 | project |
SubjectUserSid_Event4722 | project |
TargetAccount | project |
TargetSid | project |
Time_Event4722 | project |
PasswordSetAttemptDelta_Min | extend |
Reason | extend |
Computer | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
Attempt to bypass conditional access rule in Microsoft Entra ID
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0AutomationContributor" |
Cisco SE High Events Last Hour
#'Find events from Cisco Secure Endpoint that are of High severity in the last hour.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
severity_s | eq |
| field:"okta::severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CountInLastHour | summarize |
computer_hostname_s | summarize |
computer_links_trajectory_s | summarize |
date_t | summarize |
event_type_s | summarize |
Cisco WSA - Unscannable file or scan error
#'Detects unscanned downloaded file.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AmpFileName | is_not_null | field:"AmpFileName" kind:is_not_null | |
AmpScanningVerdict | in |
| field:"AmpScanningVerdict" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
FileCustomEntity | extend |
IPCustomEntity | extend |
Deimos Component Execution
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Collection | |
| Exfiltration |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceEvents action AmsiScriptContent: AMSI script content captured |
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 =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 7: extend
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"AmsiScriptContent" |
AdditionalFields | ends_with |
| field:"AdditionalFields" kind:ends_with |
InitiatingProcessFileName | eq |
| field:"InitiatingProcessFileName" kind:eq value:"powershell.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActionType | project |
AdditionalFields | project |
DeviceName | project |
InitiatingProcessCommandLine | project |
InitiatingProcessFileName | project |
InitiatingProcessParentFileName | project |
HostName | extend |
DnsDomain | extend |
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.
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"Detect AWS IAM Users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
Detect suspicious foci token logins
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Credential Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Target application: Sign-in naming a target application |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
AppId | in |
| field:"AppId" kind:in value:"FociClientApplications" |
IncomingTokenType | eq |
| field:"IncomingTokenType" kind:eq value:"none" |
IncomingTokenType | ne |
| field:"IncomingTokenType" kind:ne value:"none" |
SecondAppDisplayName | in |
| field:"AppDisplayName" kind:in |
TimeDiff | ge |
| field:"TimeDiff" kind:ge value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AuthenticationProcessingDetails | project |
ClientAppUsed | project |
DeviceDetail | project |
FirstAppDisplayName | project |
FirstAppId | project |
FirstRequestTimeGenerated | project |
FirstResult | project |
FirstResultDescription | project |
IPAddress | project |
Identity | project |
Location | project |
LocationDetails | project |
SeconIncomingTokenType | project |
SecondAppDisplayName | project |
SecondAppId | project |
SecondAuthenticationProcessingDetails | project |
SecondDeviceDetail | project |
SecondRequestTimeGenerated | project |
SecondResult | project |
SecondResultDescription | project |
SessionId | project |
TimeDiff | project |
UserAgent | project |
Egress Defend - Dangerous Link Click
#'Defend has detected a user has clicked a dangerous link in their mailbox.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
LinksClicked | gt |
| field:"LinksClicked" kind:gt value:"0" |
ThreatLevel | eq |
| field:"ThreatLevel" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account_0_FullName | extend |
GCP Audit Logs - Detect Organization Policy Deletion or Updation
#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
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GCP | any: orgpolicy.googleapis.com (any method) |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
MethodName | match |
| field:"gcp::method_name" kind:match |
ServiceName | eq |
| field:"ServiceName" kind:eq value:"orgpolicy.googleapis.com" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AccountUPNSuffix | project |
AuthEmail | project |
CallerIpAddress | project |
InsertId | project |
LogName | project |
MethodName | project |
Permission | project |
PermissionGranted | project |
PolicyName | project |
PrincipalEmail | project |
ProjectId | project |
ResourceName | project |
ServiceName | project |
Severity | project |
TimeGenerated | project |
UserAgent | project |
GCP Audit Logs - Open Firewall Rule Created or Modified
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GCP | any: compute.googleapis.com (any method) |
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 =MethodName has "insert""Created"MethodName has "patch""Modified""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.
| Field | Kind | Values | Search |
|---|---|---|---|
GCPResourceType | eq |
| field:"gcp::resource_type" kind:eq value:"gce_firewall_rule" |
MethodName | match |
| field:"gcp::method_name" kind:match |
ServiceName | eq |
| field:"ServiceName" kind:eq value:"compute.googleapis.com" |
Severity | eq |
| field:"Severity" kind:eq value:"NOTICE" |
SourceRangeStr | in |
| field:"SourceRangeStr" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AccountUPNSuffix | project |
AllowedPortsList | project |
AllowedProtocols | project |
AuthEmail | project |
CallerIpAddress | project |
Direction | project |
FirewallRuleName | project |
InsertId | project |
LogName | project |
MethodName | project |
Network | project |
OperationType | project |
PrincipalEmail | project |
Priority | project |
ProjectId | project |
ResourceName | project |
ServiceName | project |
Severity | project |
SourceRangesList | project |
TimeGenerated | project |
UserAgent | project |
Google DNS - Exchange online autodiscover abuse
#'Detects possible Exchange online autodiscover abuse.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Query | match |
| field:"Query" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DNSCustomEntity | extend |
IPCustomEntity | extend |
Guardian- Content Access Control Allowed List Policy Violation Detection
#'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.
| Field | Kind | Values | Search |
|---|---|---|---|
PolicyViolatedControlFeature | eq |
| field:"PolicyViolatedControlFeature" kind:eq value:"Allowed List" |
Severity | eq |
| field:"Severity" kind:eq value:"Low" |
McAfee ePO - Agent Handler down
#Detects when AgentHandler is down.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| McAfee-ePolicy-Orchestrator | Event ID 16025: Agent Handler is down |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventId | eq |
| field:"EventId" kind:eq value:"16025" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostCustomEntity | extend |
IPCustomEntity | extend |
Network Port Sweep from External Network (ASIM Network Session schema)
#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
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Discovery |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"Inbound" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstPortNumber | summarize |
SrcIpAddr | summarize |
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.
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
SystemId | is_not_null | field:"SystemId" kind:is_not_null | |
TimeSinceLastIngestion | gt |
| field:"TimeSinceLastIngestion" kind:gt value:"2h" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientId | summarize |
Host | summarize |
Instance | summarize |
LastIngestionTime | summarize |
SystemId | summarize |
TimeSinceLastIngestion | extend |
LookbackPeriod | extend |
Reason | extend |
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.
'Checks if Sonrai tickets have been closed. It uses the action type to check if a ticket has been closed'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Persistence | |
| Privilege Escalation | |
| Defense Impairment | |
| Credential Access | |
| Discovery | |
| Lateral Movement | |
| Collection | |
| Command & Control | |
| Exfiltration | |
| Impact |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
action_d | eq |
| field:"action_d" kind:eq value:"2" |
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.
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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Policy | match |
| field:"policy_name_s" kind:match value:"Stale IAAS policy attachment to role" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Category | project |
Description | project |
EventID | project |
Policy | project |
Recommendation | project |
Severity | project |
Tactics | project |
URL | project |
Suspicious Powershell Commandlet Executed
#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
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceEvents action PowerShellCommand: PowerShell command executed |
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"PowerShellCommand" |
Commandlet | match |
| field:"Commandlet" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceName | project |
InitiatingProcessAccountUpn | project |
InitiatingProcessCommandLine | project |
InitiatingProcessFileName | project |
InitiatingProcessFolderPath | project |
InitiatingProcessId | project |
LocalIP | project |
TimeGenerated | project |
UPNSuffix | extend |
Username | extend |
DvcDomain | extend |
DvcHostname | extend |
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.
"Creates Microsoft Sentinel incidents for critical risk Theom alerts."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Stealth | |
| Credential Access | |
| Discovery | |
| Collection | |
| Command & Control | |
| Exfiltration | |
| Impact |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
priority_s | eq |
| field:"priority_s" kind:eq value:"P1" |
Vaults Alerts for Prancer
#'High severity vault alerts found by Prancer.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
snapshot | extend |
collection | extend |
id | extend |
path | extend |
reference | extend |
region | extend |
resourceTypes | extend |
source | extend |
structure | extend |
type | extend |