Detection rules › Kusto
Kusto rules: 3h
[Entra ID] Privilege Elevation Request Denied
#Detects denied privilege elevation requests in Microsoft Entra ID. Review the requester and the target workflow for possible abuse or misconfiguration.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add member to role request denied (PIM activation) |
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] Mass Privileged Role Change Activity Detected (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: aceee46e-8fb3-42d9-967a-aac637bcefd4
name: "[Entra ID] Privilege Elevation Request Denied"
version: 1.0.0
kind: Scheduled
description: |
Detects denied privilege elevation requests in Microsoft Entra ID. Review the requester and the target workflow for possible abuse or misconfiguration.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
- DefenseEvasion
- InitialAccess
relevantTechniques:
- T1078.004
query: |
AuditLogs
| where ActivityDisplayName =~ 'Add member to role request denied (PIM activation)'
| mv-apply ResourceItem = TargetResources on
(
where ResourceItem.type =~ "Role"
| extend Role = trim(@'"', tostring(ResourceItem.displayName))
)
| mv-apply ResourceItem = TargetResources on
(
where ResourceItem.type =~ "User"
| extend TargetUserPrincipalName = trim(@'"', tostring(ResourceItem.userPrincipalName))
)
//| where ResultReason != "RoleAssignmentExists"
| where isnotempty(InitiatedBy.user)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend
TargetName = tostring(split(TargetUserPrincipalName, '@', 0)[0]),
TargetUPNSuffix = tostring(split(TargetUserPrincipalName, '@', 1)[0])
| extend
InitiatedByName = tostring(split(InitiatingUserPrincipalName, '@', 0)[0]),
InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName, '@', 1)[0])
| project-reorder
TimeGenerated,
TargetUserPrincipalName,
Role,
OperationName,
Result,
ResultDescription
| extend
InitiatingUser = tostring(InitiatedBy.user.userPrincipalName)
| extend Source_Network_IPLocation = ""
| project
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
Alert_Name_en = "Privilege Elevation Request Denied",
Alert_Description_en=strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
" in Entra ID: ",
"",
", detected PIM role activation failure: target user: ",
iff(isnotempty(TargetUserPrincipalName), TargetUserPrincipalName, "<NoTargetUser>"),
", target role: ",
iff(isnotempty(Role), Role, "<NoRole>"),
", initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
", source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", result: ",
iff(isnotempty(Result), Result, "<NoResult>"),
" (reason: ",
iff(isnotempty(ResultDescription), ResultDescription, "<NoReason>"),
")."
),
Alert_TriageStep_en=strcat(
"1. Confirm the activation target: user: ",
iff(isnotempty(TargetUserPrincipalName), TargetUserPrincipalName, "<NoTargetUser>"),
", role: ",
iff(isnotempty(Role), Role, "<NoRole>"),
" is an expected PIM activation request.",
"2. Check the failure reason: ",
iff(isnotempty(ResultDescription), ResultDescription, "<NoReason>"),
", and determine whether it was caused by MFA/Conditional Access/approval process/insufficient permissions.",
"3. Check source risk: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
", IP:",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
" to determine whether it is a company named location/VPN/jump server, abnormal location, or uncommon source."
),
Alert_Containment_en=strcat(
"1. If determined to be a suspicious attempt, immediately restrict initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
" 's PIM activation permissions or suspend the scope of high-risk roles that can be activated. ",
"2. Immediately revoke suspicious sign-in tokens/sessions and force re-authentication (initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
"), and reset the password and re-register MFA if necessary. ",
"3. Apply stricter Conditional Access to source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
" (named locations/compliant devices/MFA only), or block it first to stop repeated attempts. ",
"4. If the failure was caused by the approval process/policy, suspend activation requests outside the change window and notify administrators for confirmation."
),
Alert_Remediation_en=strcat(
"1. Strengthen PIM governance: require approval and justification for high-privilege role activation, and limit the scope of activatable roles (least privilege/separation of duties). ",
"2. Strengthen Conditional Access: enforce MFA, compliant devices, and named locations for PIM/management-plane operations, and directly block abnormal IPs/locations. ",
"3. Establish alerts and automated response: set thresholds and automated SOAR for PIM activation failures (notification, IP blocking, account suspension). "
),
Alert_Time_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Alert_Time_UTC0 = TimeGenerated,
Event_Action = OperationName,
Event_Status = Result,
Event_Description = ResultDescription,
Source_Identity_FullName = InitiatingUser,
Source_Identity_ID = InitiatingAadUserId,
// Source_Identity_Name = InitiatingName,
Source_Identity_Type = iff(isnotempty(InitiatingUser), "User", "Service"),
Source_Network_IPAddress = InitiatingIpAddress,
Target_Identity_FullName = TargetUserPrincipalName,
Source_Resource_ID = InitiatingAppServicePrincipalId,
Source_Resource_Name = InitiatingAppName,
//Target_Identity_Name = InvitedUserName,
Target_Identity_Type = "User",
Target_Identity_DomainType = iff(TargetUserPrincipalName contains "EXT", 'External', 'Internal'),
Target_Resource_ID = "",
Target_Resource_Name = "Microsoft Entra ID",
Target_Resource_Type = "Microsoft Entra ID"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Source_Identity_FullName
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Target_Identity_FullName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Source_Network_IPAddress
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where ActivityDisplayName =~ 'Add member to role request denied (PIM activation)'
Stage 3: kusto:mv-apply
| mv-apply ResourceItem = TargetResources on
(
where ResourceItem.type =~ "Role"
| extend Role = trim(@'"', tostring(ResourceItem.displayName))
)
Stage 4: kusto:mv-apply
| mv-apply ResourceItem = TargetResources on
(
where ResourceItem.type =~ "User"
| extend TargetUserPrincipalName = trim(@'"', tostring(ResourceItem.userPrincipalName))
)
Stage 5: where
| where isnotempty(InitiatedBy.user)
Stage 6: extend (7 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend
TargetName = tostring(split(TargetUserPrincipalName, '@', 0)[0]),
TargetUPNSuffix = tostring(split(TargetUserPrincipalName, '@', 1)[0])
| extend
InitiatedByName = tostring(split(InitiatingUserPrincipalName, '@', 0)[0]),
InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName, '@', 1)[0])
Stage 7: project-reorder
| project-reorder
TimeGenerated,
TargetUserPrincipalName,
Role,
OperationName,
Result,
ResultDescription
Stage 8: extend
| extend
InitiatingUser = tostring(InitiatedBy.user.userPrincipalName)
Stage 9: extend
| extend Source_Network_IPLocation = ""
Stage 10: project
| project
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
Alert_Name_en = "Privilege Elevation Request Denied",
Alert_Description_en=strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
" in Entra ID: ",
"",
", detected PIM role activation failure: target user: ",
iff(isnotempty(TargetUserPrincipalName), TargetUserPrincipalName, "<NoTargetUser>"),
", target role: ",
iff(isnotempty(Role), Role, "<NoRole>"),
", initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
", source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
", result: ",
iff(isnotempty(Result), Result, "<NoResult>"),
" (reason: ",
iff(isnotempty(ResultDescription), ResultDescription, "<NoReason>"),
")."
),
Alert_TriageStep_en=strcat(
"1. Confirm the activation target: user: ",
iff(isnotempty(TargetUserPrincipalName), TargetUserPrincipalName, "<NoTargetUser>"),
", role: ",
iff(isnotempty(Role), Role, "<NoRole>"),
" is an expected PIM activation request.",
"2. Check the failure reason: ",
iff(isnotempty(ResultDescription), ResultDescription, "<NoReason>"),
", and determine whether it was caused by MFA/Conditional Access/approval process/insufficient permissions.",
"3. Check source risk: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
", IP:",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
" to determine whether it is a company named location/VPN/jump server, abnormal location, or uncommon source."
),
Alert_Containment_en=strcat(
"1. If determined to be a suspicious attempt, immediately restrict initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
" 's PIM activation permissions or suspend the scope of high-risk roles that can be activated. ",
"2. Immediately revoke suspicious sign-in tokens/sessions and force re-authentication (initiator: ",
iff(isnotempty(InitiatingUser), InitiatingUser, "<NoInitiator>"),
"), and reset the password and re-register MFA if necessary. ",
"3. Apply stricter Conditional Access to source IP: ",
iff(isnotempty(InitiatingIpAddress), InitiatingIpAddress, "<NoIP>"),
" (named locations/compliant devices/MFA only), or block it first to stop repeated attempts. ",
"4. If the failure was caused by the approval process/policy, suspend activation requests outside the change window and notify administrators for confirmation."
),
Alert_Remediation_en=strcat(
"1. Strengthen PIM governance: require approval and justification for high-privilege role activation, and limit the scope of activatable roles (least privilege/separation of duties). ",
"2. Strengthen Conditional Access: enforce MFA, compliant devices, and named locations for PIM/management-plane operations, and directly block abnormal IPs/locations. ",
"3. Establish alerts and automated response: set thresholds and automated SOAR for PIM activation failures (notification, IP blocking, account suspension). "
),
Alert_Time_TW = datetime_utc_to_local(TimeGenerated, 'Asia/Taipei'),
Alert_Time_UTC0 = TimeGenerated,
Event_Action = OperationName,
Event_Status = Result,
Event_Description = ResultDescription,
Source_Identity_FullName = InitiatingUser,
Source_Identity_ID = InitiatingAadUserId,
Source_Identity_Type = iff(isnotempty(InitiatingUser), "User", "Service"),
Source_Network_IPAddress = InitiatingIpAddress,
Target_Identity_FullName = TargetUserPrincipalName,
Source_Resource_ID = InitiatingAppServicePrincipalId,
Source_Resource_Name = InitiatingAppName,
Target_Identity_Type = "User",
Target_Identity_DomainType = iff(TargetUserPrincipalName contains "EXT", 'External', 'Internal'),
Target_Resource_ID = "",
Target_Resource_Name = "Microsoft Entra ID",
Target_Resource_Type = "Microsoft Entra ID"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | eq |
| field:"azure_ad::activity_display_name" kind:eq value:"Add member to role request denied (PIM activation)" |
InitiatedBy.user | is_not_null | field:"InitiatedBy.user" kind:is_not_null | |
ResourceItem.type | eq |
| field:"ResourceItem.type" kind:eq |
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_Description | project |
Event_Status | project |
Source_Identity_FullName | project |
Source_Identity_ID | project |
Source_Identity_Type | project |
Source_Network_IPAddress | project |
Source_Resource_ID | project |
Source_Resource_Name | project |
Target_Identity_DomainType | project |
Target_Identity_FullName | project |
Target_Identity_Type | project |
Target_Resource_ID | project |
Target_Resource_Name | project |
Target_Resource_Type | project |
A host is potentially running a hacking tool (ASIM Web Session schema)
#'This rule identifies a web request with a user agent header known to belong to a hacking tool. This indicates a hacking tool is used on the host.<br>You can add custom hacking tool indicating User-Agent headers using a watchlist, for more information refer to the UnusualUserAgents Watchlist. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Discovery | |
| Lateral Movement | |
| Collection | |
| Command & Control | |
| Exfiltration |
Rule Body
id: 3f0c20d5-6228-48ef-92f3-9ff7822c1954
name: A host is potentially running a hacking tool (ASIM Web Session schema)
description: |
'This rule identifies a web request with a user agent header known to belong to a hacking tool. This indicates a hacking tool is used on the host.<br>You can add custom hacking tool indicating User-Agent headers using a watchlist, for more information refer to the [UnusualUserAgents Watchlist](https://aka.ms/ASimUnusualUserAgentsWatchlist).
This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
severity: Medium
tags:
- ParentName: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/CiscoUmbrella/CiscoUmbrellaHackToolUserAgentDetected.yaml
version: 1.0.0
- Schema: ASimWebSession
SchemaVersion: 0.2.1
requiredDataConnectors:
- connectorId: SquidProxy
dataTypes:
- SquidProxy_CL
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- Discovery
- LateralMovement
- Collection
- CommandAndControl
- Exfiltration
relevantTechniques:
- T1059
- T1046
- T1021
- T1557
- T1102
- T1020
query: |
let threatCategory="Hacking Tool";
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
let knownUserAgents=toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let customUserAgents=toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
let fullUAList = array_concat(knownUserAgents,customUserAgents);
_Im_WebSession(httpuseragent_has_any=fullUAList)
| project SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1])
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SrcUsername
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
alertDetailsOverride:
alertDisplayNameFormat: 'Host {{SrcIpAddr}} is potentially running a hacking tool'
alertDescriptionFormat: 'The host at address {{SrcIpAddr}} sent an HTTP request to the URL {{Url}} with the HTTP user agent header {{HttpUserAgent}}. This user agent is known to be used by a hacking tool and indicates suspicious activity on the host.'
customDetails:
UserAgent: HttpUserAgent
eventGroupingSettings:
aggregationKind: AlertPerResult
version: 1.1.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yaron
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Parameters
let threatCategory = "Hacking Tool";
let fullUAList = array_concat(knownUserAgents,customUserAgents);
Let Binding: knownUserAgentsIndicators
let knownUserAgentsIndicators = materialize(externaldata(UserAgent:string, Category:string)
[ @"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/UnusualUserAgents.csv"]
with(format="csv", ignoreFirstRecord=True));
Let Binding: knownUserAgents
let knownUserAgents = toscalar(knownUserAgentsIndicators | where Category==threatCategory | where isnotempty(UserAgent) | summarize make_list(UserAgent));
Let Binding: customUserAgents
let customUserAgents = toscalar(_GetWatchlist("UnusualUserAgents") | where SearchKey==threatCategory | extend UserAgent=column_ifexists("UserAgent","") | where isnotempty(UserAgent) | summarize make_list(UserAgent));
Stage 1: source
_Im_WebSession(httpuseragent_has_any=fullUAList)
Stage 2: project
| project SrcIpAddr, Url, TimeGenerated, HttpUserAgent, SrcUsername
Stage 3: extend
| extend AccountName = tostring(split(SrcUsername, "@")[0]), AccountUPNSuffix = tostring(split(SrcUsername, "@")[1])
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HttpUserAgent | project |
SrcIpAddr | project |
SrcUsername | project |
TimeGenerated | project |
Url | project |
AccountName | extend |
AccountUPNSuffix | extend |
Account created from non-approved sources
#This query looks for an account being created from a domain that is not regularly seen in a tenant. Attackers may attempt to add accounts from these sources as a means of establishing persistant access to an environment. Created accounts should be investigated to confirm expected creation. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-accounts
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add user |
| Entra ID | SigninLogs sign-in event: Sign-in record (SigninLogs, any result) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
- Azure AD External Guest User Invited (Splunk)
Rule Body
id: 99d589fa-7337-40d7-91a0-c96d0c4fa437
name: Account created from non-approved sources
description: |
'This query looks for an account being created from a domain that is not regularly seen in a tenant.
Attackers may attempt to add accounts from these sources as a means of establishing persistant access to an environment.
Created accounts should be investigated to confirm expected creation.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-accounts'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- AuditLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1136.003
tags:
- AADSecOpsGuide
query: |
let core_domains = (SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| extend domain = tolower(split(UserPrincipalName, "@")[1])
| summarize by tostring(domain));
let alternative_domains = (SigninLogs
| where TimeGenerated > ago(7d)
| where isnotempty(AlternateSignInName)
| where ResultType == 0
| extend domain = tolower(split(AlternateSignInName, "@")[1])
| summarize by tostring(domain));
AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName =~ "Add User"
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend UserAdded = tostring(TargetResources[0].userPrincipalName)
| extend UserAddedDomain = case(
UserAdded has "#EXT#", tostring(split(tostring(split(UserAdded, "#EXT#")[0]), "_")[1]),
UserAdded !has "#EXT#", tostring(split(UserAdded, "@")[1]),
UserAdded)
| where UserAddedDomain !in (core_domains) and UserAddedDomain !in (alternative_domains)
| extend AddedByName = case(
InitiatingUserPrincipalName has "#EXT#", tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[0]),
InitiatingUserPrincipalName !has "#EXT#", tostring(split(InitiatingUserPrincipalName, "@")[0]),
InitiatingUserPrincipalName)
| extend AddedByUPNSuffix = case(
InitiatingUserPrincipalName has "#EXT#", tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[1]),
InitiatingUserPrincipalName !has "#EXT#", tostring(split(InitiatingUserPrincipalName, "@")[1]),
InitiatingUserPrincipalName)
| extend UserAddedName = case(
UserAdded has "#EXT#", tostring(split(tostring(split(UserAdded, "#EXT#")[0]), "_")[0]),
UserAdded !has "#EXT#", tostring(split(UserAdded, "@")[0]),
UserAdded)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: AddedByName
- identifier: UPNSuffix
columnName: AddedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserAdded
- identifier: Name
columnName: UserAddedName
- identifier: UPNSuffix
columnName: UserAddedDomain
version: 1.2.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Let Binding: core_domains
let core_domains = (SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| extend domain = tolower(split(UserPrincipalName, "@")[1])
| summarize by tostring(domain));
Let Binding: alternative_domains
let alternative_domains = (SigninLogs
| where TimeGenerated > ago(7d)
| where isnotempty(AlternateSignInName)
| where ResultType == 0
| extend domain = tolower(split(AlternateSignInName, "@")[1])
| summarize by tostring(domain));
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(1d)
Stage 3: where
| where OperationName =~ "Add User"
Stage 4: extend (7 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend UserAdded = tostring(TargetResources[0].userPrincipalName)
| extend UserAddedDomain = case(
UserAdded has "#EXT#", tostring(split(tostring(split(UserAdded, "#EXT#")[0]), "_")[1]),
UserAdded !has "#EXT#", tostring(split(UserAdded, "@")[1]),
UserAdded)
Stage 5: where
| where UserAddedDomain !in (core_domains) and UserAddedDomain !in (alternative_domains)
Stage 6: extend (3 consecutive steps)
| extend AddedByName = case(
InitiatingUserPrincipalName has "#EXT#", tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[0]),
InitiatingUserPrincipalName !has "#EXT#", tostring(split(InitiatingUserPrincipalName, "@")[0]),
InitiatingUserPrincipalName)
| extend AddedByUPNSuffix = case(
InitiatingUserPrincipalName has "#EXT#", tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[1]),
InitiatingUserPrincipalName !has "#EXT#", tostring(split(InitiatingUserPrincipalName, "@")[1]),
InitiatingUserPrincipalName)
| extend UserAddedName = case(
UserAdded has "#EXT#", tostring(split(tostring(split(UserAdded, "#EXT#")[0]), "_")[0]),
UserAdded !has "#EXT#", tostring(split(UserAdded, "@")[0]),
UserAdded)
AddedByName =InitiatingUserPrincipalName has "#EXT#"tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[0])not (InitiatingUserPrincipalName has "#EXT#")tostring(split(InitiatingUserPrincipalName, "@")[0])InitiatingUserPrincipalNameIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Add User" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserAddedDomain | eq | alternative_domains | excludes:UserAddedDomain field:"UserAddedDomain" value:"alternative_domains" |
UserAddedDomain | eq | core_domains | excludes:UserAddedDomain field:"UserAddedDomain" value:"core_domains" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
UserAdded | extend |
UserAddedDomain | extend |
AddedByName | extend |
AddedByUPNSuffix | extend |
UserAddedName | extend |
AWS Security Hub - Detect IAM Policies allowing full administrative privileges
#This query detects AWS IAM policies that allow full administrative ("*") privileges in violation of AWS Security Hub control IAM.1. Overly permissive policies increase the risk of privilege escalation and unauthorized access.
MITRE ATT&CK Coverage
Rule Body
id: de1f71d2-d127-439d-a8a2-e64d3187298a
name: AWS Security Hub - Detect IAM Policies allowing full administrative privileges
description: |
This query detects AWS IAM policies that allow full administrative ("*") privileges in violation of AWS Security Hub control IAM.1.
Overly permissive policies increase the risk of privilege escalation and unauthorized access.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AWSSecurityHub
dataTypes:
- AWSSecurityHubFindings
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098.003
- T1078.001
tags:
- PCI DSS v3.2.1
- NIST 800-53 r5
- CIS AWS Foundations Benchmark v1.4.0
query: |
AWSSecurityHubFindings
| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
// Match Security Hub control IAM.1 by GeneratorId or explicit SecurityControlId/Title
| where tostring(AwsSecurityFindingGeneratorId) == "security-control/IAM.1"
or tostring(ComplianceSecurityControlId) == "IAM.1"
| extend
IAMPolicyId = tostring(Resources[0].Details.AwsIamPolicy.PolicyId),
IAMPolicyName = tostring(Resources[0].Details.AwsIamPolicy.PolicyName),
IAMPolicyDescription = tostring(Resources[0].Details.AwsIamPolicy.Description)
| summarize TimeGenerated = max(TimeGenerated)
by
AwsAccountId,
AwsRegion,
AwsSecurityFindingTitle,
AwsSecurityFindingDescription,
AwsSecurityFindingId,
ComplianceSecurityControlId,
IAMPolicyId,
IAMPolicyName,
IAMPolicyDescription
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AwsAccountId
- identifier: CloudAppAccountId
columnName: AwsAccountId
customDetails:
ComplianceControlId: ComplianceSecurityControlId
Region: AwsRegion
FindingId: AwsSecurityFindingId
alertDetailsOverride:
alertDisplayNameFormat: "AWS Account {{AwsAccountId}} has IAM Policy with full administrative privileges"
alertDescriptionFormat: "AWS Account {{AwsAccountId}} has IAM Policy {{IAMPolicyId}} with full administrative privileges."
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSSecurityHubFindings
Stage 2: where
| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
Stage 3: where
| where tostring(AwsSecurityFindingGeneratorId) == "security-control/IAM.1"
or tostring(ComplianceSecurityControlId) == "IAM.1"
Stage 4: extend
| extend
IAMPolicyId = tostring(Resources[0].Details.AwsIamPolicy.PolicyId),
IAMPolicyName = tostring(Resources[0].Details.AwsIamPolicy.PolicyName),
IAMPolicyDescription = tostring(Resources[0].Details.AwsIamPolicy.Description)
Stage 5: summarize
| summarize TimeGenerated = max(TimeGenerated)
by
AwsAccountId,
AwsRegion,
AwsSecurityFindingTitle,
AwsSecurityFindingDescription,
AwsSecurityFindingId,
ComplianceSecurityControlId,
IAMPolicyId,
IAMPolicyName,
IAMPolicyDescription
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AwsAccountId | summarize |
AwsRegion | summarize |
AwsSecurityFindingDescription | summarize |
AwsSecurityFindingId | summarize |
AwsSecurityFindingTitle | summarize |
ComplianceSecurityControlId | summarize |
IAMPolicyDescription | summarize |
IAMPolicyId | summarize |
IAMPolicyName | summarize |
TimeGenerated | summarize |
Base64 encoded Windows process command-lines (Normalized Process Events)
#Identifies instances of a base64 encoded PE file header seen in the process command line parameter. To use this analytics rule, make sure you have deployed the ASIM normalization parsers
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Stealth |
Telemetry Coverage
Rule Body
id: f8b3c49c-4087-499b-920f-0dcfaff0cbca
name: Base64 encoded Windows process command-lines (Normalized Process Events)
description: |
'Identifies instances of a base64 encoded PE file header seen in the process command line parameter.
To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimProcessEvent)'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- DefenseEvasion
relevantTechniques:
- T1059
- T1027
- T1140
tags:
- Id: ca67c83e-7fff-4127-a3e3-1af66d6d4cad
version: 1.0.0
- Schema: ASIMProcessEvent
SchemaVersion: 0.1.0
query: |
imProcessCreate
| where CommandLine contains "TVqQAAMAAAAEAAA"
| where isnotempty(Process)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Dvc, ActorUsername, Process, CommandLine, ActingProcessName, EventVendor, EventProduct
| extend AccountName = tostring(split(ActorUsername, @'\')[1]), AccountNTDomain = tostring(split(ActorUsername, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
| project-away DomainIndex
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Dvc
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Yuval Naor
support:
tier: Community
categories:
domains: [ "Security - Threat Protection" ]
Stages and Predicates
Stage 1: source
imProcessCreate
Stage 2: where
| where CommandLine contains "TVqQAAMAAAAEAAA"
Stage 3: where
| where isnotempty(Process)
Stage 4: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Dvc, ActorUsername, Process, CommandLine, ActingProcessName, EventVendor, EventProduct
Stage 5: extend (3 consecutive steps)
| extend AccountName = tostring(split(ActorUsername, @'\')[1]), AccountNTDomain = tostring(split(ActorUsername, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
Stage 6: project-away
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | contains |
| field:"CommandLine" kind:contains value:"TVqQAAMAAAAEAAA" |
Process | is_not_null | field:"Process" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActingProcessName | summarize |
ActorUsername | summarize |
CommandLine | summarize |
Dvc | summarize |
EndTime | summarize |
EventProduct | summarize |
EventVendor | summarize |
Process | summarize |
StartTime | summarize |
AccountNTDomain | extend |
AccountName | extend |
HostName | extend |
HostNameDomain | extend |
BloodHound Attack Path Finding - Logons From Tier Zero 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.
BloodHound Attack Path Finding - Logons From Tier Zero Users
Rule Body
id: cf22c2f4-fdc6-465f-8d71-561af44bd475
name: BloodHound Attack Path Finding - Logons From Tier Zero Users
description: BloodHound Attack Path Finding - Logons From Tier Zero Users
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"T0Logins\""
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 == "T0Logins"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0Logins" |
BloodHound Attack Path Finding - Ownership of Tier Zero Principal
#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 - Ownership of Tier Zero Principal
Rule Body
id: d902d7c1-8a9c-469a-9899-7222214a0663
name: BloodHound Attack Path Finding - Ownership of Tier Zero Principal
description: BloodHound Attack Path Finding - Ownership of Tier Zero Principal
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 == "AzureT0Owns"
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 == "AzureT0Owns"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0Owns" |
Cisco SE - Ransomware Activity
#'This rule is triggered when possible ransomware activity is detected on host.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: c9629114-0f49-4b50-9f1b-345287b2eebf
name: Cisco SE - Ransomware Activity
description: |
'This rule is triggered when possible ransomware activity is detected on host.'
severity: High
requiredDataConnectors:
- connectorId: CiscoSecureEndpoint
dataTypes:
- CiscoSecureEndpoint
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Impact
relevantTechniques:
- T1486
query: |
CiscoSecureEndpoint
| where EventMessage has 'Suspected ransomware'
| extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostCustomEntity
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CiscoSecureEndpoint
Stage 2: where
| where EventMessage has 'Suspected ransomware'
Stage 3: extend
| extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"Suspected ransomware" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostCustomEntity | extend |
MalwareCustomEntity | extend |
Cloud Replica Permanent Failover Performed by Tenant
#Detects permanent failover of a cloud replica initiated by a tenant. This might indicate disaster recovery activity or issues with primary systems.
Rule Body
id: 1f1634e5-585e-4187-ac5b-6d0e157bd2ea
name: Cloud Replica Permanent Failover Performed by Tenant
description: Detects permanent failover of a cloud replica initiated by a tenant.
This might indicate disaster recovery activity or issues with primary systems.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 27000\n| extend VmRef = extract(\"\
VmRef=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| extend TenantID = extract(\"TenantID=\\\
\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| extend TenantID = extract(\"TenantID=\\\
\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| extend HostID = extract(\"HostID=\\\"([^\\\
\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date = format_datetime(TimeGenerated,\
\ 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n EventId = instanceId,\n\
\ [\"Replica Name\"] = VmRef,\n [\"Tenant ID\"] = TenantID,\n [\"Data SourceID\"\
] = HostID,\n MessageDetails = Description,\n Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 27000
Stage 3: extend (4 consecutive steps)
| extend VmRef = extract("VmRef=\"([^\"]*)\"", 1, SyslogMessage)
| extend TenantID = extract("TenantID=\"([^\"]*)\"", 1, SyslogMessage)
| extend TenantID = extract("TenantID=\"([^\"]*)\"", 1, SyslogMessage)
| extend HostID = extract("HostID=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["Replica Name"] = VmRef,
["Tenant ID"] = TenantID,
["Data SourceID"] = HostID,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"27000" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Data SourceID | project |
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Replica Name | project |
Severity | project |
Tenant ID | project |
CTE - Sudden Block Spike Detected
#A sudden spike in blocked URLs/domains on a single device may indicate an active phishing or malware campaign.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: fbbf3418-f1d8-4bd5-b7da-137a70af6bce
name: CTE - Sudden Block Spike Detected
description: |
A sudden spike in blocked URLs/domains on a single device may indicate an active phishing or malware campaign.
severity: High
requiredDataConnectors:
- connectorId: cybereinforce_cte
dataTypes:
- CybereinforceCTE_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1189
query: |
CybereinforceCTE_CL
| where EventType == "block.url"
| extend d = parse_json(Details)
| extend
DeviceId = tostring(d.deviceId),
HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
DeviceUser = tostring(d.deviceUser),
DeviceOs = tostring(d.deviceOs),
DeviceVersion = tostring(d.deviceVersion),
RuleId = tostring(d.ruleId),
RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
Pattern = tostring(d.pattern),
Url = tostring(d.url),
Action = tostring(d.action),
IsIoc = tobool(d.ioc),
ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
UserAgent = tostring(d.userAgent)
| summarize BlockCount=count(), SampleUrls=make_set(Url, 20), SampleIps=make_set(ClientIp, 20), LastHit=max(TimeGenerated) by bin(TimeGenerated, 10m), HostName, DeviceId, DeviceUser, DeviceOs, TenantId
| where BlockCount >= 15
| extend Url = tostring(SampleUrls[0]), ClientIp = tostring(SampleIps[0])
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CybereinforceCTE_CL
Stage 2: where
| where EventType == "block.url"
Stage 3: extend
| extend d = parse_json(Details)
Stage 4: extend
| extend
DeviceId = tostring(d.deviceId),
HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
DeviceUser = tostring(d.deviceUser),
DeviceOs = tostring(d.deviceOs),
DeviceVersion = tostring(d.deviceVersion),
RuleId = tostring(d.ruleId),
RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
Pattern = tostring(d.pattern),
Url = tostring(d.url),
Action = tostring(d.action),
IsIoc = tobool(d.ioc),
ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
UserAgent = tostring(d.userAgent)
HostName =isnotempty(tostring(d.hostname))tostring(d.hostname)strcat("cte-device-", substring(EntityId, 0, 8))RuleName =isnotempty(tostring(d.ruleName))tostring(d.ruleName)"Pattern Match"Stage 5: summarize
| summarize BlockCount=count(), SampleUrls=make_set(Url, 20), SampleIps=make_set(ClientIp, 20), LastHit=max(TimeGenerated) by bin(TimeGenerated, 10m), HostName, DeviceId, DeviceUser, DeviceOs, TenantId
Stage 6: where
| where BlockCount >= 15
Stage 7: extend
| extend Url = tostring(SampleUrls[0]), ClientIp = tostring(SampleIps[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
BlockCount | ge |
| field:"BlockCount" kind:ge value:"15" |
EventType | eq |
| field:"EventType" kind:eq value:"block.url" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
BlockCount | summarize |
DeviceId | summarize |
DeviceOs | summarize |
DeviceUser | summarize |
HostName | summarize |
LastHit | summarize |
SampleIps | summarize |
SampleUrls | summarize |
TenantId | summarize |
ClientIp | extend |
Url | extend |
CYFIRMA - High severity Trojan Network Indicators - Block Recommended Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"This analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Persistence | |
| Credential Access | |
| Command & Control | |
| Impact |
Rule Body
id: 441204ca-274f-43d2-aeda-53409b94f447
name: CYFIRMA - High severity Trojan Network Indicators - Block Recommended Rule
description: |
"This analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
- connectorId: CyfirmaCyberIntelligenceDC
dataTypes:
- CyfirmaIndicators_CL
query: |
//Trojan Network Indicators - Block Recommended
let timeFrame= 5m;
CyfirmaIndicators_CL
| where ConfidenceScore >= 80
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'Trojan'
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
| mv-expand extensionKeys
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
- Impact
- Persistence
- DefenseEvasion
- CredentialAccess
- CommandAndControl
- Execution
- InitialAccess
relevantTechniques:
- T1496
- T1053
- T1555
- T1090
- T1071
- T1204
- T1189
- T1053.005
- T1555.003
- T1071.001
- T1204.001
alertDetailsOverride:
alertDisplayNameFormat: "High-Confidence Trojan Network Indicators - Block Recommended Rule - {{name}} "
alertDescriptionFormat: "{{Description}} - {{name}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
customDetails:
ThreatActors: ThreatActors
Sources: Sources
RecommendedActions: RecommendedActions
Roles: Roles
Country: Country
Description: Description
ConfidenceScore: ConfidenceScore
SecurityVendors: SecurityVendors
IndicatorID: IndicatorID
Created: created
Modified: modified
ValidFrom: valid_from
Tags: Tags
ThreatType: ThreatType
TimeGenerated: TimeGenerated
IPAbuse: IPAbuse
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv4
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv6
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaIndicators_CL
Stage 2: where
| where ConfidenceScore >= 80
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'Trojan'
Stage 3: extend (6 consecutive steps)
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
Stage 4: mv-expand
| mv-expand extensionKeys
Stage 5: extend (4 consecutive steps)
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
Stage 6: project
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfidenceScore | ge |
| field:"ConfidenceScore" kind:ge value:"80" |
RecommendedActions | match |
| field:"RecommendedActions" kind:match value:"Block" |
Roles | match |
| field:"Roles" kind:match value:"Trojan" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
CYFIRMA - Social and Public Exposure - Source Code Exposure on Public Repositories Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"This rule triggers when CYFIRMA detects source code related to internal or enterprise domains exposed on public platforms like GitHub. Such exposure may lead to intellectual property leakage or help adversaries understand internal systems, increasing the risk of targeted attacks."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Credential Access | |
| Discovery |
Rule Body
id: 28e315a3-725d-4261-a6c2-e597d51541f4
name: CYFIRMA - Social and Public Exposure - Source Code Exposure on Public Repositories Rule
description: |
"This rule triggers when CYFIRMA detects source code related to internal or enterprise domains exposed on public platforms like GitHub.
Such exposure may lead to intellectual property leakage or help adversaries understand internal systems, increasing the risk of targeted attacks."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
- connectorId: CyfirmaDigitalRiskAlertsConnector
dataTypes:
- CyfirmaSPESourceCodeAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- ResourceDevelopment
- CredentialAccess
- Discovery
relevantTechniques:
- T1587.001
- T1606.001
- T1082
query: |
// Medium severity - Social and Public Exposure - Source Code Exposure on Public Repositories
let timeFrame = 5m;
CyfirmaSPESourceCodeAlerts_CL
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Source=source,
Impact=impact,
Recommendation=recommendation,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
ProductName,
ProviderName,
AlertTitle
customDetails:
AssetType: AssetType
AssetValue: AssetValue
RiskScore: RiskScore
Description: Description
FirstSeen: FirstSeen
LastSeen: LastSeen
TimeGenerated: TimeGenerated
AlertUID: AlertUID
UID: UID
Impact: Impact
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA Medium Severity Alert - Source Code Exposure on Public Repositories - {{AlertTitle}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaSPESourceCodeAlerts_CL
Stage 2: where
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Source=source,
Impact=impact,
Recommendation=recommendation,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
ProductName,
ProviderName,
AlertTitle
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertTitle | project |
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
TimeGenerated | project |
UID | project |
Failed host logons but success logon to AzureAD
#Identifies a list of IP addresses with a minimum number(default of 5) of failed logon attempts to remote hosts. Uses that list to identify any successful logons to Microsoft Entra ID from these IPs within the same timeframe.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
Rule Body
id: 1ce5e766-26ab-4616-b7c8-3b33ae321e80
name: Failed host logons but success logon to AzureAD
description: |
'Identifies a list of IP addresses with a minimum number(default of 5) of failed logon attempts to remote hosts.
Uses that list to identify any successful logons to Microsoft Entra ID from these IPs within the same timeframe.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: AzureActiveDirectory
dataTypes:
- AADNonInteractiveUserSignInLogs
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
- T1110
query: |
//Adjust this threshold to fit environment
let signin_threshold = 5;
//Make a list of IPs with failed Windows host logins above threshold
let win_fails =
SecurityEvent
| where EventID == 4625
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
let wef_fails =
WindowsEvent
| where EventID == 4625
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
//Make a list of IPs with failed *nix host logins above threshold
let nix_fails =
Syslog
| where Facility contains 'auth' and ProcessName != 'sudo' and SyslogMessage has 'from' and not(SyslogMessage has_any ('Disconnecting', 'Disconnected', 'Accepted', 'disconnect', @'[preauth]'))
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP != "" and SourceIP != "127.0.0.1"
| summarize count() by SourceIP
| where count_ > signin_threshold
| summarize make_list(SourceIP);
//See if any of the IPs with failed host logins hve had a sucessful Azure AD login
let aadFunc = (tableName:string){
table(tableName)
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (win_fails) or IPAddress in (nix_fails) or IPAddress in (wef_fails)
| extend Reason= "Multiple failed host logins from IP address with successful Azure AD login"
| extend timestamp = TimeGenerated, Type = Type
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = 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: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.1.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let signin_threshold = 5;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
Let Binding: win_fails
let win_fails = SecurityEvent
| where EventID == 4625
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
Let Binding: wef_fails
let wef_fails = WindowsEvent
| where EventID == 4625
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
Let Binding: nix_fails
let nix_fails = Syslog
| where Facility contains 'auth' and ProcessName != 'sudo' and SyslogMessage has 'from' and not(SyslogMessage has_any ('Disconnecting', 'Disconnected', 'Accepted', 'disconnect', @'[preauth]'))
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP != "" and SourceIP != "127.0.0.1"
| summarize count() by SourceIP
| where count_ > signin_threshold
| summarize make_list(SourceIP);
Let Binding: aadFunc
let aadFunc = (tableName:string){
table(tableName)
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (win_fails) or IPAddress in (nix_fails) or IPAddress in (wef_fails)
| extend Reason= "Multiple failed host logins from IP address with successful Azure AD login"
| extend timestamp = TimeGenerated, Type = Type
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = 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
Host Deleted
#Detects when a host is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.
Rule Body
id: 5601e894-2d3c-42fd-bc71-f0350b1b0bc6
name: Host Deleted
description: Detects when a host is deleted from Veeam Backup & Replication. This
might indicate unauthorized removal of critical components.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 28500\n| extend Name = extract(\"\
Name=\\\"(?<Name>[^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date = format_datetime(TimeGenerated,\
\ 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n EventId = instanceId,\n\
\ [\"User Name\"] = user,\n [\"Data SourceName\"] = Name,\n MessageDetails\
\ = Description,\n Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 28500
Stage 3: extend
| extend Name = extract("Name=\"(?<Name>[^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
["Data SourceName"] = Name,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"28500" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Data SourceName | project |
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
Mercury - Domain, Hash and IP IOCs - August 2022
#Identifies a match across various data feeds for domains, hashes and IP IOC related to Mercury Reference: https://www.microsoft.com/security/blog/2022/08/25/mercury-leveraging-log4j-2-vulnerabilities-in-unpatched-systems-to-target-israeli-organizations/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
Rule Body
id: ae10c588-7ff7-486c-9920-ab8b0bdb6ede
name: Mercury - Domain, Hash and IP IOCs - August 2022
description: |
'Identifies a match across various data feeds for domains, hashes and IP IOC related to Mercury
Reference: https://www.microsoft.com/security/blog/2022/08/25/mercury-leveraging-log4j-2-vulnerabilities-in-unpatched-systems-to-target-israeli-organizations/'
severity: High
requiredDataConnectors:
- connectorId: DNS
dataTypes:
- DnsEvents
- connectorId: AzureMonitor(VMInsights)
dataTypes:
- VMConnection
- connectorId: F5
dataTypes:
- CommonSecurityLog
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
- connectorId: CheckPoint
dataTypes:
- CommonSecurityLog
- connectorId: CEF
dataTypes:
- CommonSecurityLog
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- DeviceFileEvents
- DeviceImageLoadEvents
- connectorId: Office365
dataTypes:
- OfficeActivity
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- AZFWApplicationRule
- AZFWDnsQuery
- connectorId: WindowsFirewall
dataTypes:
- WindowsFirewall
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
tags:
- Mercury
- Schema: ASIMFileEvent
SchemaVersion: 0.1.0
query: |
let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Mercury_August2022.csv"] with (format="csv", ignoreFirstRecord=True);
let sha256Hashes = (iocs | where Type =~ "sha256" | project IoC);
let IPList = (iocs | where Type =~ "ip"| project IoC);
let domains = (iocs | where Type =~ "domainname"| project IoC);
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
(union isfuzzy=true
(CommonSecurityLog
| where SourceIP in (IPList) or DestinationIP in (IPList) or DestinationHostName has_any (domains) or RequestURL has_any (domains) or Message has_any (IPList)
| parse Message with * '(' DNSName ')' *
| project TimeGenerated, SourceIP, DestinationIP, Message, SourceUserID, RequestURL, DNSName, Type
| extend MessageIP = extract(IPRegex, 0, Message), RequestIP = extract(IPRegex, 0, RequestURL)
| extend IPMatch = case(SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", MessageIP in (IPList), "Message", RequestURL has_any (domains), "RequestUrl", "NoMatch")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, IPMatch == "Message", MessageIP, "NoMatch")
| extend AccountName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
),
(DnsEvents
| where IPAddresses in (IPList) or Name in~ (domains)
| project TimeGenerated, Computer, IPAddresses, Name, ClientIP, Type
| extend IPAddress = IPAddresses, DNSName = Name, Computer
),
(VMConnection
| where SourceIp in (IPList) or DestinationIp in (IPList) or RemoteDnsCanonicalNames has_any (domains)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| project TimeGenerated, Computer, Direction, ProcessName, SourceIp, DestinationIp, DestinationPort, RemoteDnsQuestions, DNSName,BytesSent, BytesReceived, RemoteCountry, Type
| extend IPMatch = case( SourceIp in (IPList), "SourceIP", DestinationIp in (IPList), "DestinationIP", "None")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIp, IPMatch == "DestinationIP", DestinationIp, "NoMatch"), File = ProcessName
),
(Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 3
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend SourceIP = tostring(EventDetail.[9].["#text"]), DestinationIP = tostring(EventDetail.[14].["#text"]), Image = tostring(EventDetail.[4].["#text"])
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, SourceIP, DestinationIP, Image, UserName, Computer, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend AccountNT = UserName, File = tostring(split(Image, '\\', -1)[-1]), IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
),
(OfficeActivity
| where ClientIP in (IPList)
| project TimeGenerated, UserAgent, Operation, RecordType, UserId, ClientIP, Type
| extend IPAddress = ClientIP, AccountUPN = UserId, AccountUPNName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
),
(DeviceNetworkEvents
| where RemoteUrl has_any (domains) or RemoteIP in (IPList) or InitiatingProcessSHA256 in (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessSHA256, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, RemoteIP, RemoteUrl, RemotePort, LocalIP, Type
| extend IPAddress = RemoteIP, FileHash = InitiatingProcessSHA256
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(WindowsFirewall
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, Computer, CommunicationDirection, SourceIP, DestinationIP, SourcePort, DestinationPort, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where isnotempty(DestinationHost)
| where DestinationHost has_any (IPList) or DestinationHost has_any (domains)
| extend DNSName = DestinationHost, IPAddress = SourceHost
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallNetworkRule"
| where msg_s has_any (IPList)
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " to " TargetIP ":" TargetPortInt:int *
| parse kind=regex flags=U msg_s with * ". Action\\: " Action1a "\\."
| parse msg_s with * ". Policy: " Policy ". Rule Collection Group: " RuleCollectionGroup "." *
| parse msg_s with * " Rule Collection: " RuleCollection ". Rule: " Rule
| extend IPAddress = SourceIP
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallDnsProxy"
| where msg_s has_any (domains)
| parse msg_s with "DNS Request: " SourceIP ":" SourcePortInt:int " - " QueryID:int " " RequestType " " RequestClass " " hostname ". " protocol " " details
| extend
ResponseDuration = extract("[0-9]*.?[0-9]+s$", 0, msg_s),
SourcePort = tostring(SourcePortInt),
QueryID = tostring(QueryID)
| project TimeGenerated,SourceIP,hostname,RequestType,ResponseDuration,details,msg_s
| extend IPAddress = SourceIP
),
(AZFWApplicationRule
| where Fqdn has_any (domains) or Fqdn has_any (IPList)
| extend IPAddress = SourceIp
),
(AZFWDnsQuery
| where isnotempty(QueryName)
| where QueryName has_any (domains)
| extend DNSName = QueryName, IPAddress = SourceIp
),
(AZFWNetworkRule
| where DestinationIp has_any (IPList)
| extend IPAddress = SourceIp
),
(CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend Algorithm = "SHA256", FileHash = tostring(FileHash), AccountUPN = SourceUserID, AccountUPNName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
),
(imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
),
(DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend Image = EventDetail.[4].["#text"], CommandLine = EventDetail.[10].["#text"], Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)
| project TimeGenerated, EventDetail, AccountNT = UserName, Computer, Type, Source, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source), FileHash = tostring(Hashes[1]), Algorithm = tostring(Hashes[0])
)
)
| extend AccountNTName = tostring(split(AccountNT, "\\")[1]), AccountNTDomain = tostring(split(AccountNT, "\\")[0])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountUPN
- identifier: Name
columnName: AccountUPNName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountNT
- identifier: Name
columnName: AccountNTName
- identifier: UPNSuffix
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: FileHash
fieldMappings:
- identifier: Algorithm
columnName: Algorithm
- identifier: Value
columnName: FileHash
version: 1.1.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Ajeet Prakash
support:
tier: Community
categories:
domains: [ "Security - Threat Intelligence" ]
Stages and Predicates
Parameters
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
Let Binding: iocs
let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Mercury_August2022.csv"] with (format="csv", ignoreFirstRecord=True);
Let Binding: sha256Hashes
let sha256Hashes = (iocs | where Type =~ "sha256" | project IoC);
Let Binding: IPList
let IPList = (iocs | where Type =~ "ip"| project IoC);
Let Binding: domains
let domains = (iocs | where Type =~ "domainname"| project IoC);
Union isfuzzy=true (18 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: CommonSecurityLog, DnsEvents, VMConnection, Event, OfficeActivity, DeviceNetworkEvents, WindowsFirewall, AzureDiagnostics, AzureDiagnostics, AzureDiagnostics, AZFWApplicationRule, AZFWDnsQuery, AZFWNetworkRule, CommonSecurityLog, imFileEvent, DeviceFileEvents, DeviceImageLoadEvents, Event
Leg 1: CommonSecurityLog
CommonSecurityLog
| where SourceIP in (IPList) or DestinationIP in (IPList) or DestinationHostName has_any (domains) or RequestURL has_any (domains) or Message has_any (IPList)
| parse Message with * '(' DNSName ')' *
| project TimeGenerated, SourceIP, DestinationIP, Message, SourceUserID, RequestURL, DNSName, Type
| extend MessageIP = extract(IPRegex, 0, Message), RequestIP = extract(IPRegex, 0, RequestURL)
| extend IPMatch = case(SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", MessageIP in (IPList), "Message", RequestURL has_any (domains), "RequestUrl", "NoMatch")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, IPMatch == "Message", MessageIP, "NoMatch")
| extend AccountName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
Leg 2: DnsEvents
DnsEvents
| where IPAddresses in (IPList) or Name in~ (domains)
| project TimeGenerated, Computer, IPAddresses, Name, ClientIP, Type
| extend IPAddress = IPAddresses, DNSName = Name, Computer
Leg 3: VMConnection
VMConnection
| where SourceIp in (IPList) or DestinationIp in (IPList) or RemoteDnsCanonicalNames has_any (domains)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| project TimeGenerated, Computer, Direction, ProcessName, SourceIp, DestinationIp, DestinationPort, RemoteDnsQuestions, DNSName,BytesSent, BytesReceived, RemoteCountry, Type
| extend IPMatch = case( SourceIp in (IPList), "SourceIP", DestinationIp in (IPList), "DestinationIP", "None")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIp, IPMatch == "DestinationIP", DestinationIp, "NoMatch"), File = ProcessName
Leg 4: Event
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 3
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend SourceIP = tostring(EventDetail.[9].["#text"]), DestinationIP = tostring(EventDetail.[14].["#text"]), Image = tostring(EventDetail.[4].["#text"])
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, SourceIP, DestinationIP, Image, UserName, Computer, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend AccountNT = UserName, File = tostring(split(Image, '\\', -1)[-1]), IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
Leg 5: OfficeActivity
OfficeActivity
| where ClientIP in (IPList)
| project TimeGenerated, UserAgent, Operation, RecordType, UserId, ClientIP, Type
| extend IPAddress = ClientIP, AccountUPN = UserId, AccountUPNName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
Leg 6: DeviceNetworkEvents
DeviceNetworkEvents
| where RemoteUrl has_any (domains) or RemoteIP in (IPList) or InitiatingProcessSHA256 in (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessSHA256, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, RemoteIP, RemoteUrl, RemotePort, LocalIP, Type
| extend IPAddress = RemoteIP, FileHash = InitiatingProcessSHA256
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
Leg 7: WindowsFirewall
WindowsFirewall
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, Computer, CommunicationDirection, SourceIP, DestinationIP, SourcePort, DestinationPort, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
Leg 8: AzureDiagnostics
AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where isnotempty(DestinationHost)
| where DestinationHost has_any (IPList) or DestinationHost has_any (domains)
| extend DNSName = DestinationHost, IPAddress = SourceHost
Leg 9: AzureDiagnostics
AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallNetworkRule"
| where msg_s has_any (IPList)
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " to " TargetIP ":" TargetPortInt:int *
| parse kind=regex flags=U msg_s with * ". Action\\: " Action1a "\\."
| parse msg_s with * ". Policy: " Policy ". Rule Collection Group: " RuleCollectionGroup "." *
| parse msg_s with * " Rule Collection: " RuleCollection ". Rule: " Rule
| extend IPAddress = SourceIP
Leg 10: AzureDiagnostics
AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallDnsProxy"
| where msg_s has_any (domains)
| parse msg_s with "DNS Request: " SourceIP ":" SourcePortInt:int " - " QueryID:int " " RequestType " " RequestClass " " hostname ". " protocol " " details
| extend
ResponseDuration = extract("[0-9]*.?[0-9]+s$", 0, msg_s),
SourcePort = tostring(SourcePortInt),
QueryID = tostring(QueryID)
| project TimeGenerated,SourceIP,hostname,RequestType,ResponseDuration,details,msg_s
| extend IPAddress = SourceIP
Leg 11: AZFWApplicationRule
AZFWApplicationRule
| where Fqdn has_any (domains) or Fqdn has_any (IPList)
| extend IPAddress = SourceIp
Leg 12: AZFWDnsQuery
AZFWDnsQuery
| where isnotempty(QueryName)
| where QueryName has_any (domains)
| extend DNSName = QueryName, IPAddress = SourceIp
Leg 13: AZFWNetworkRule
AZFWNetworkRule
| where DestinationIp has_any (IPList)
| extend IPAddress = SourceIp
Leg 14: CommonSecurityLog
CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend Algorithm = "SHA256", FileHash = tostring(FileHash), AccountUPN = SourceUserID, AccountUPNName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
Leg 15: imFileEvent
imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
Leg 16: DeviceFileEvents
DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
Leg 17: DeviceImageLoadEvents
DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
Leg 18: Event
Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend Image = EventDetail.[4].["#text"], CommandLine = EventDetail.[10].["#text"], Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)
| project TimeGenerated, EventDetail, AccountNT = UserName, Computer, Type, Source, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source), FileHash = tostring(Hashes[1]), Algorithm = tostring(Hashes[0])
Applied to the Combined Result
| extend AccountNTName = tostring(split(AccountNT, "\\")[1]), AccountNTDomain = tostring(split(AccountNT, "\\")[0])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountNT | project |
CommandLine | project |
Computer | project |
EventDetail | project |
Hashes | project |
Image | project |
Source | project |
TimeGenerated | project |
Type | extend |
Algorithm | extend |
FileHash | extend |
AccountNTDomain | extend |
AccountNTName | extend |
HostName | extend |
HostNameDomain | extend |
NordPass - User fails authentication
#This will alert you if a user fails to log in to their NordPass account or SSO authentication three or more times in the last 24 hours.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 27b261dc-68f3-489a-944f-bc252e0c1960
name: NordPass - User fails authentication
kind: Scheduled
version: 1.0.0
displayName: User fails authentication
description: |
This will alert you if a user fails to log in to their NordPass account or SSO authentication three or more times in the last 24 hours.
severity: High
query: |
let threshold = 2;
let filteredData = materialize(
NordPassEventLogs_CL
| where event_type == "login" and action == "user_login_failed"
);
let users = filteredData
| summarize Count = count() by user_email
| where Count > threshold
| project user_email;
filteredData
| where user_email in (users)
| extend TargetEmail = user_email, IPAddress = metadata.ip_address, Provider = metadata.provider
requiredDataConnectors:
- connectorId: NordPass
dataTypes:
- NordPassEventLogs_CL
queryFrequency: 5m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 2
tactics:
- CredentialAccess
relevantTechniques:
- T1110
- T1556.003
customDetails:
Provider: Provider
incidentConfiguration:
createIncident: false
entityMappings:
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: TargetEmail
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
Stages and Predicates
Parameters
let threshold = 2;
let filteredData is inlined into the numbered stages below.
Let Binding: users
let users = filteredData
| summarize Count = count() by user_email
| where Count > threshold
| project user_email;
Stages 1 to 2 define let filteredData as the rule's main pipeline source. Stages 3 to 4 use it.
Stage 1: source
NordPassEventLogs_CL
Stage 2: where
| where event_type == "login" and action == "user_login_failed"
Stage 3: where
filteredData
| where user_email in (users)
Stage 4: extend
| extend TargetEmail = user_email, IPAddress = metadata.ip_address, Provider = metadata.provider
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"user_login_failed" |
event_type | eq |
| field:"event_type" kind:eq value:"login" |
user_email | in |
| field:"user_email" kind:in value:"users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPAddress | extend |
Provider | extend |
TargetEmail | extend |
NRT Modified domain federation trust settings
#This will alert when a user or application modifies the federation settings on the domain or Update domain authentication from Managed to Federated. For example, this alert will trigger when a new Active Directory Federated Service (ADFS) TrustedRealm object, such as a signing certificate, is added to the domain. Modification to domain federation settings should be rare. Confirm the added or modified target domain/URL is legitimate administrator behavior. To understand why an authorized user may update settings for a federated domain in Office 365, Azure, or Intune, see: https://docs.microsoft.com/office365/troubleshoot/active-directory/update-federated-domain-office-365. For details on security realms that accept security tokens, see the ADFS Proxy Protocol (MS-ADFSPP) specification: https://docs.microsoft.com/openspecs/windows_protocols/ms-adfspp/e7b9ea73-1980-4318-96a6-da559486664b. For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Credential Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Set domain authentication |
| Azure | Set federation settings on domain |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Domain Federation Trust Settings Modified (Kusto)
- Azure AD New Custom Domain Added (Splunk)
- Azure AD New Federated Domain Added (Splunk)
- Azure Domain Federation Settings Modified (Sigma)
- Entra ID Custom Domain Added or Verified (Elastic)
- Entra ID Domain Federation Configuration Change (Elastic)
- Modified domain federation trust settings (Kusto)
- New onmicrosoft domain added to tenant (Kusto)
Rule Body
id: 8540c842-5bbc-4a24-9fb2-a836c0e55a51
name: NRT Modified domain federation trust settings
description: |
'This will alert when a user or application modifies the federation settings on the domain or Update domain authentication from Managed to Federated.
For example, this alert will trigger when a new Active Directory Federated Service (ADFS) TrustedRealm object, such as a signing certificate, is added to the domain.
Modification to domain federation settings should be rare. Confirm the added or modified target domain/URL is legitimate administrator behavior.
To understand why an authorized user may update settings for a federated domain in Office 365, Azure, or Intune, see: https://docs.microsoft.com/office365/troubleshoot/active-directory/update-federated-domain-office-365.
For details on security realms that accept security tokens, see the ADFS Proxy Protocol (MS-ADFSPP) specification: https://docs.microsoft.com/openspecs/windows_protocols/ms-adfspp/e7b9ea73-1980-4318-96a6-da559486664b.
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- CredentialAccess
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1555
- T1098
query: |
AuditLogs
| where OperationName =~ "Set federation settings on domain" or OperationName =~ "Set domain authentication"
//| where Result =~ "success" // commenting out, as it may be interesting to capture failed attempts
| mv-expand TargetResources
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
| mv-apply Property = modifiedProperties on
(
where Property.displayName =~ "LiveType"
| extend targetDisplayName = tostring(Property.displayName),
NewDomainValue = tostring(Property.newValue)
)
| extend Federated = iif(OperationName =~ "Set domain authentication", iif(NewDomainValue has "Federated", True, False), True)
| where Federated == True
| mv-expand AdditionalDetails
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, AADOperationType, targetDisplayName, Result, UserAgent, CorrelationId, TenantId, AADTenantId
| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.3
kind: NRT
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName =~ "Set federation settings on domain" or OperationName =~ "Set domain authentication"
Stage 3: mv-expand
| mv-expand TargetResources
Stage 4: extend
| extend modifiedProperties = parse_json(TargetResources).modifiedProperties
Stage 5: kusto:mv-apply
| mv-apply Property = modifiedProperties on
(
where Property.displayName =~ "LiveType"
| extend targetDisplayName = tostring(Property.displayName),
NewDomainValue = tostring(Property.newValue)
)
Stage 6: extend
| extend Federated = iif(OperationName =~ "Set domain authentication", iif(NewDomainValue has "Federated", True, False), True)
Stage 7: where
| where Federated == True
Stage 8: mv-expand
| mv-expand AdditionalDetails
Stage 9: kusto:mv-apply
| mv-apply AdditionalDetail = AdditionalDetails on
(
where AdditionalDetail.key =~ "User-Agent"
| extend UserAgent = tostring(AdditionalDetail.value)
)
Stage 10: extend (5 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
Stage 11: project-reorder
| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, AADOperationType, targetDisplayName, Result, UserAgent, CorrelationId, TenantId, AADTenantId
Stage 12: extend
| extend Name = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AdditionalDetail.key | eq |
| field:"AdditionalDetail.key" kind:eq value:"User-Agent" |
Federated | eq |
| field:"Federated" kind:eq value:"true" |
OperationName | eq |
| field:"OperationName" kind:eq |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"LiveType" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
modifiedProperties | extend |
Federated | extend |
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
Name | extend |
UPNSuffix | extend |
NRT Privileged Role Assigned Outside PIM
#Identifies a privileged role being assigned to a user outside of PIM Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add member to role |
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] Mass Privileged Role Change Activity Detected (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)
Rule Body
id: 14f6da04-2f96-44ee-9210-9ccc1be6401e
name: NRT Privileged Role Assigned Outside PIM
description: |
'Identifies a privileged role being assigned to a user outside of PIM
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
query: |
AuditLogs
| where Category =~ "RoleManagement"
| where OperationName has "Add member to role outside of PIM"
or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatedByName
- identifier: UPNSuffix
columnName: InitiatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.3
kind: NRT
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where OperationName has "Add member to role outside of PIM"
or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")
Stage 4: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
)
Stage 5: extend (7 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Identity | ne |
| field:"Identity" kind:ne |
LoggedByService | eq |
| field:"azure_ad::logged_by_service" kind:eq value:"Core Directory" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Add member to role" |
OperationName | match |
| field:"OperationName" kind:match value:"Add member to role outside of PIM" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"User" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
TargetName | extend |
TargetUPNSuffix | extend |
InitiatedByName | extend |
InitiatedByUPNSuffix | extend |
Pathlock TDnR - Transaction and Report Statistics
#Detects security-relevant transaction and report usage statistics from SAP STAD, forwarded by Pathlock Threat Detection and Response. Unusual usage patterns such as mass execution of sensitive transactions, high-frequency report runs, or access by unexpected users may indicate reconnaissance or data harvesting.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c70
name: Pathlock TDnR - Transaction and Report Statistics
kind: Scheduled
description: >-
Detects security-relevant transaction and report usage statistics from SAP STAD, forwarded by
Pathlock Threat Detection and Response. Unusual usage patterns such as mass execution of sensitive
transactions, high-frequency report runs, or access by unexpected users may indicate reconnaissance
or data harvesting.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1082
query: |
Pathlock_TDnR_CL
| where DataSource == "TCODE_STATISTIC"
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Bname
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
eventGroupingSettings:
aggregationKind: SingleAlert
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: AnyAlert
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0
Stages and Predicates
Stage 1: source
Pathlock_TDnR_CL
Stage 2: where
| where DataSource == "TCODE_STATISTIC"
Stage 3: project
| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"TCODE_STATISTIC" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
PRODAFT USTA - Non-expired payment card exposed
#'Identifies a new PRODAFT USTA Payment Card Fraud Intelligence ticket that exposes a payment card whose expiration date is still in the future. A non-expired card is immediately usable for fraudulent transactions and is therefore higher risk than an already-expired card. The full card number is never stored - only the BIN, last 4 digits, brand, and length are retained. Prioritize reissue of the affected card with the issuing bank and monitor for fraudulent activity.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 674429c9-a858-436c-95b8-5808024ebd01
name: PRODAFT USTA - Non-expired payment card exposed
description: |
'Identifies a new PRODAFT USTA Payment Card Fraud Intelligence ticket that exposes a
payment card whose expiration date is still in the future. A non-expired card is
immediately usable for fraudulent transactions and is therefore higher risk than an
already-expired card. The full card number is never stored - only the BIN, last 4 digits,
brand, and length are retained. Prioritize reissue of the affected card with the issuing
bank and monitor for fraudulent activity.'
severity: High
status: Available
tactics:
- Impact
relevantTechniques:
- T1657
requiredDataConnectors:
- connectorId: PRODAFTUstaPCFICCPDefinition
dataTypes:
- PRODAFTUstaCompromisedCards_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
query: |
PRODAFTUstaCompromisedCards
| where Created > ago(1h)
| where ExpirationDate > now()
| project
TimeGenerated,
Created,
TicketId,
CompanyName,
CardBrand,
CardBin,
CardLast4,
CardMasked,
CardLength,
ExpirationDate,
Status
customDetails:
Ticket: TicketId
Company: CompanyName
Brand: CardBrand
Card: CardMasked
Expiration: ExpirationDate
Status: Status
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AnyAlert
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
PRODAFTUstaCompromisedCards
Stage 2: where
| where Created > ago(1h)
Stage 3: where
| where ExpirationDate > now()
Stage 4: project
| project
TimeGenerated,
Created,
TicketId,
CompanyName,
CardBrand,
CardBin,
CardLast4,
CardMasked,
CardLength,
ExpirationDate,
Status
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CardBin | project |
CardBrand | project |
CardLast4 | project |
CardLength | project |
CardMasked | project |
CompanyName | project |
Created | project |
ExpirationDate | project |
Status | project |
TicketId | project |
TimeGenerated | project |
Recorded Future Alerts Incident Creation
#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.
'Create Recorded Future Alerts in Microsoft Defender'
Rule Body
id: 0aded97e-4e2d-4e4d-93cf-4a7e45c7bab3
name: Recorded Future Alerts Incident Creation
description: |
'Create Recorded Future Alerts in Microsoft Defender'
severity: Medium
status: Available
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
RecordedFuturePortalAlerts_CL
| where TimeGenerated >= now(-1h)
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: 'Alert: {{AlertName_s}}'
alertDescriptionFormat: |
_Recorded Future Alert_
AlertID:
{{AlertID_s}}
Triggered: {{Triggered_t}}
AI Summary: {{AISummary_s}}
alertDynamicProperties:
- alertProperty: AlertLink
value: URL_s
customDetails:
RFLabel: SourceSystem
RuleName: RuleName_s
RuleId: RuleId_s
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: Selected
groupByCustomDetails:
- RuleId
version: 1.0.0
kind: NRT
Stages and Predicates
Stage 1: source
RecordedFuturePortalAlerts_CL
Stage 2: where
| where TimeGenerated >= now(-1h)
SAP BTP - Cloud Identity Service application configuration monitor
#Identifies CRUD operations on Application (SSO Domain/Service Provider) configurations within SAP Cloud Identity Service. This includes both SAML 2.0 and OpenID Connect applications. Unauthorized application creation could indicate an attacker establishing persistent access through a rogue federated application.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Credential Access |
Rule Body
id: 3f8a2c5e-7b9d-4e1a-8f6c-2d4b9a1e3c7f
kind: Scheduled
name: SAP BTP - Cloud Identity Service application configuration monitor
description: |
Identifies CRUD operations on Application (SSO Domain/Service Provider) configurations
within SAP Cloud Identity Service. This includes both SAML 2.0 and OpenID Connect applications.
Unauthorized application creation could indicate an attacker establishing persistent access
through a rogue federated application.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPBTPAuditEvents
dataTypes:
- SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- PrivilegeEscalation
relevantTechniques:
- T1606
- T1556
- T1134
query: |
SAPBTPAuditLog_CL
| extend data_s = tostring(Message.data)
| extend
action = extract(@"action=""([^""]+)""", 1, data_s),
state = extract(@"state=""([^""]+)""", 1, data_s),
objectType = extract(@"objectType=""([^""]+)""", 1, data_s),
serviceProviderName = extract(@"serviceProviderName=""([^""]+)""", 1, data_s),
ipAddress = extract(@"ipAddress=""([^""]+)""", 1, data_s),
// Extract payload JSON for federation type
payloadJson = extract(@"\{""payload"":\{([^}]+)\}\}", 1, data_s)
| extend federationType = extract(@"""type"":""([^""]+)""", 1, payloadJson)
| where objectType == "ssoDomain"
| where state == "successful"
| where action in ("create", "update", "delete")
| extend MessageText = case(
action == "create", strcat("Application '", serviceProviderName, "' (", federationType, ") was created"),
action == "update", strcat("Application '", serviceProviderName, "' (", federationType, ") was updated"),
action == "delete", strcat("Application '", serviceProviderName, "' was deleted"),
"Unclassified operation on application"
)
| project
UpdatedOn,
UserName,
MessageText,
ServiceProviderName = serviceProviderName,
FederationType = federationType,
Action = action,
Tenant,
ipAddress,
CloudApp = "SAP Cloud Identity Service"
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: CloudApp
alertDetailsOverride:
alertDisplayNameFormat: 'SAP Cloud Identity Service: {{MessageText}}'
alertDescriptionFormat: |
{{MessageText}} by {{UserName}}. Identity provider name: {{ServiceProviderName}}
This could indicate:
- Legitimate administrative change to federated applications
- Unauthorized application registration for persistent access
- Rogue SAML/OIDC application added by attacker
customDetails:
ServiceProviderName: ServiceProviderName
FederationType: FederationType
Action: Action
SourceIP: ipAddress
version: 1.0.1
Stages and Predicates
Stage 1: source
SAPBTPAuditLog_CL
Stage 2: extend (3 consecutive steps)
| extend data_s = tostring(Message.data)
| extend
action = extract(@"action=""([^""]+)""", 1, data_s),
state = extract(@"state=""([^""]+)""", 1, data_s),
objectType = extract(@"objectType=""([^""]+)""", 1, data_s),
serviceProviderName = extract(@"serviceProviderName=""([^""]+)""", 1, data_s),
ipAddress = extract(@"ipAddress=""([^""]+)""", 1, data_s),
payloadJson = extract(@"\{""payload"":\{([^}]+)\}\}", 1, data_s)
| extend federationType = extract(@"""type"":""([^""]+)""", 1, payloadJson)
Stage 3: where
| where objectType == "ssoDomain"
Stage 4: where
| where state == "successful"
Stage 5: where
| where action in ("create", "update", "delete")
Stage 6: extend
| extend MessageText = case(
action == "create", strcat("Application '", serviceProviderName, "' (", federationType, ") was created"),
action == "update", strcat("Application '", serviceProviderName, "' (", federationType, ") was updated"),
action == "delete", strcat("Application '", serviceProviderName, "' was deleted"),
"Unclassified operation on application"
)
MessageText =action == "create"strcat("Application '", serviceProviderName, "' (", federationType, ") was created")action == "update"strcat("Application '", serviceProviderName, "' (", federationType, ") was updated")action == "delete"strcat("Application '", serviceProviderName, "' was deleted")"Unclassified operation on application"Stage 7: project
| project
UpdatedOn,
UserName,
MessageText,
ServiceProviderName = serviceProviderName,
FederationType = federationType,
Action = action,
Tenant,
ipAddress,
CloudApp = "SAP Cloud Identity Service"
Stage 8: extend
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | in |
| field:"action" kind:in |
objectType | eq |
| field:"objectType" kind:eq value:"ssoDomain" |
state | eq |
| field:"state" kind:eq value:"successful" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
CloudApp | project |
FederationType | project |
MessageText | project |
ServiceProviderName | project |
Tenant | project |
UpdatedOn | project |
UserName | project |
ipAddress | project |
AccountName | extend |
UPNSuffix | extend |
SAP BTP - Cloud Integration package import or transport
#Identifies import and transport operations for integration packages and artifacts in SAP Cloud Integration. Packages contain integration flows, mappings, scripts, and other artifacts that can be imported from external sources or transported between tenants. Unauthorized package operations could indicate: - Supply chain attack through malicious package import - Lateral movement between environments via artifact transport - Introduction of backdoors or rogue integration logic
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence |
Rule Body
id: c3d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f
kind: Scheduled
name: SAP BTP - Cloud Integration package import or transport
description: |
Identifies import and transport operations for integration packages and artifacts in
SAP Cloud Integration. Packages contain integration flows, mappings, scripts, and other
artifacts that can be imported from external sources or transported between tenants.
Unauthorized package operations could indicate:
- Supply chain attack through malicious package import
- Lateral movement between environments via artifact transport
- Introduction of backdoors or rogue integration logic
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SAPBTPAuditEvents
dataTypes:
- SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
relevantTechniques:
- T1195
- T1546
query: |
let packageActions = dynamic(["Package_Import_Started", "Package_Import_Completed", "Package_Transport_Started", "Package_Transport_Completed", "Artifact_Transport_Started", "Artifact_Transport_Completed"]);
SAPBTPAuditLog_CL
| where Category == "audit.security-events"
| extend data_s = tostring(Message.data),
ipAddress = tostring(Message.ip)
| extend parsedData = parse_json(data_s)
| extend action = tostring(parsedData.action),
objectType = tostring(parsedData.objectType),
objectId = tostring(parsedData.objectId)
| where action in (packageActions)
| extend operationType = case(
action contains "Import", "Import",
action contains "Transport", "Transport",
"Unknown"
),
operationStatus = case(
action endswith "Started", "Started",
action endswith "Completed", "Completed",
"Unknown"
)
| extend MessageText = strcat(objectType, " '", objectId, "' ", tolower(operationType), " ", tolower(operationStatus))
| project
UpdatedOn,
UserName,
MessageText,
ObjectType = objectType,
ObjectId = objectId,
Action = action,
OperationType = operationType,
OperationStatus = operationStatus,
Tenant,
ipAddress,
CloudApp = "SAP Cloud Integration"
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: CloudApp
alertDetailsOverride:
alertDisplayNameFormat: 'SAP Cloud Integration: {{MessageText}}'
alertDescriptionFormat: |
{{MessageText}} by {{UserName}} from IP {{ipAddress}}.
This could indicate:
- Legitimate package management and deployment
- Import of malicious integration content from untrusted sources
- Unauthorized transport of artifacts between environments
customDetails:
ObjectType: ObjectType
ObjectId: ObjectId
Action: Action
OperationType: OperationType
OperationStatus: OperationStatus
SourceIP: ipAddress
version: 1.0.1
Stages and Predicates
Let Binding: packageActions
let packageActions = dynamic(["Package_Import_Started", "Package_Import_Completed", "Package_Transport_Started", "Package_Transport_Completed", "Artifact_Transport_Started", "Artifact_Transport_Completed"]);
Stage 1: source
SAPBTPAuditLog_CL
Stage 2: where
| where Category == "audit.security-events"
Stage 3: extend (3 consecutive steps)
| extend data_s = tostring(Message.data),
ipAddress = tostring(Message.ip)
| extend parsedData = parse_json(data_s)
| extend action = tostring(parsedData.action),
objectType = tostring(parsedData.objectType),
objectId = tostring(parsedData.objectId)
Stage 4: where
| where action in (packageActions)
Stage 5: extend
| extend operationType = case(
action contains "Import", "Import",
action contains "Transport", "Transport",
"Unknown"
),
operationStatus = case(
action endswith "Started", "Started",
action endswith "Completed", "Completed",
"Unknown"
)
operationStatus =action endswith "Started""Started"action endswith "Completed""Completed""Unknown"operationType =action contains "Import""Import"action contains "Transport""Transport""Unknown"Stage 6: extend
| extend MessageText = strcat(objectType, " '", objectId, "' ", tolower(operationType), " ", tolower(operationStatus))
Stage 7: project
| project
UpdatedOn,
UserName,
MessageText,
ObjectType = objectType,
ObjectId = objectId,
Action = action,
OperationType = operationType,
OperationStatus = operationStatus,
Tenant,
ipAddress,
CloudApp = "SAP Cloud Integration"
Stage 8: extend
| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"audit.security-events" |
action | in |
| field:"action" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
CloudApp | project |
MessageText | project |
ObjectId | project |
ObjectType | project |
OperationStatus | project |
OperationType | project |
Tenant | project |
UpdatedOn | project |
UserName | project |
ipAddress | project |
AccountName | extend |
UPNSuffix | extend |
Service Principal Assigned App Role With Sensitive Access
#Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read. A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions. Ensure that any assignment to a Service Principal is valid and appropriate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add app role assignment to service principal |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule Body
id: dd78a122-d377-415a-afe9-f22e08d2112c
name: Service Principal Assigned App Role With Sensitive Access
description: |
'Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read.
A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions.
Ensure that any assignment to a Service Principal is valid and appropriate.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
// Add other permissions to this list as needed
let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
let auditList =
AuditLogs
| where OperationName =~ "Add app role assignment to service principal"
| mv-expand TargetResources[0].modifiedProperties
| extend TargetResources_0_modifiedProperties = column_ifexists("TargetResources_0_modifiedProperties", '')
| where isnotempty(TargetResources_0_modifiedProperties)
;
let detailsList = auditList
| where TargetResources_0_modifiedProperties.displayName =~ "AppRole.Value" or TargetResources_0_modifiedProperties.displayName =~ "DelegatedPermissionGrant.Scope"
| extend Permissions = split((parse_json(tostring(TargetResources_0_modifiedProperties.newValue))), " ")
| where Permissions has_any (permissions)
| summarize AddedPermissions=make_set(Permissions,200) by CorrelationId
| join kind=inner auditList on CorrelationId
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend displayName = tostring(TargetResources_0_modifiedProperties.displayName), newValue = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
| where displayName == "ServicePrincipal.ObjectID" or displayName == "ServicePrincipal.DisplayName"
| extend displayName = case(displayName == "ServicePrincipal.ObjectID", "ServicePrincipalObjectID", displayName == "ServicePrincipal.DisplayName", "ServicePrincipalDisplayName", displayName)
| project TimeGenerated, CorrelationId, Id, AddedPermissions = tostring(AddedPermissions), InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, displayName, newValue
;
detailsList | project Id, displayName, newValue
| evaluate pivot(displayName, make_set(newValue))
| join kind=inner detailsList on Id
| extend ServicePrincipalObjectID = todynamic(column_ifexists("ServicePrincipalObjectID", "")), ServicePrincipalDisplayName = todynamic(column_ifexists("ServicePrincipalDisplayName", ""))
| mv-expand ServicePrincipalObjectID, ServicePrincipalDisplayName
| project-away Id1, displayName, newValue
| extend ServicePrincipalObjectID = tostring(ServicePrincipalObjectID), ServicePrincipalDisplayName = tostring(ServicePrincipalDisplayName)
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), EventIds = make_set(Id,200) by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- identifier: ObjectGuid
columnName: ServicePrincipalObjectID
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIPAddress
version: 2.0.0
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
let auditList and let detailsList are inlined into the numbered stages below.
Let Binding: permissions
let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
Stages 1 to 14 define let detailsList as the rule's main pipeline source. Stages 15 to 23 use it.
Stage 1: source
AuditLogs
Stage 2: where
where OperationName =~ "Add app role assignment to service principal"
Stage 3: mv-expand
mv-expand TargetResources[0].modifiedProperties
Stage 4: extend
extend TargetResources_0_modifiedProperties
Stage 5: where
where isnotempty(TargetResources_0_modifiedProperties)
Stage 6: where
where (TargetResources_0_modifiedProperties.displayName =~ "AppRole.Value" or TargetResources_0_modifiedProperties.displayName =~ "DelegatedPermissionGrant.Scope")
Stage 7: extend
extend Permissions
Stage 8: where
where (Permissions has ".All" or Permissions has "ReadWrite" or Permissions has "Mail." or Permissions has "offline_access" or Permissions has "Files.Read" or Permissions has "Notes.Read" or Permissions has "ChannelMessage.Read" or Permissions has "Chat.Read" or Permissions has "TeamsActivity.Read" or Permissions has "Group.Read" or Permissions has "EWS.AccessAsUser.All" or Permissions has "EAS.AccessAsUser.All")
Stage 9: summarize
summarize AddedPermissions by CorrelationId
Stage 10: join
| join kind=inner auditList on CorrelationId
Stage 11: extend (7 consecutive steps)
extend InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, displayName, newValue
Stage 12: where
where (displayName == "ServicePrincipal.DisplayName" or displayName == "ServicePrincipal.ObjectID")
Stage 13: extend
extend displayName
displayName =displayName == "ServicePrincipal.ObjectID""ServicePrincipalObjectID"displayName == "ServicePrincipal.DisplayName""ServicePrincipalDisplayName"displayNameStage 14: project
project AddedPermissions, CorrelationId, Id, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, TimeGenerated, displayName, newValue
Stage 15: project
project Id, displayName, newValue
Stage 16: evaluate
evaluate
Stage 17: join
join kind=inner (detailsList) on Id
Stage 18: extend
extend ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 19: mv-expand
mv-expand ServicePrincipalObjectID
Stage 20: project-away
project-away Id1, displayName, newValue
Stage 21: extend
extend ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 22: summarize
summarize EventIds, FirstSeen, LastSeen by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 23: extend
extend InitiatingAccountName, InitiatingAccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Add app role assignment to service principal" |
Permissions | match |
| field:"Permissions" kind:match |
TargetResources_0_modifiedProperties | is_not_null | field:"TargetResources_0_modifiedProperties" kind:is_not_null | |
TargetResources_0_modifiedProperties.displayName | eq |
| field:"TargetResources_0_modifiedProperties.displayName" kind:eq |
displayName | eq |
| field:"displayName" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AddedPermissions | summarize |
CorrelationId | summarize |
EventIds | summarize |
FirstSeen | summarize |
InitiatedBy | summarize |
InitiatingAadUserId | summarize |
InitiatingAppName | summarize |
InitiatingAppServicePrincipalId | summarize |
InitiatingIPAddress | summarize |
InitiatingUserPrincipalName | summarize |
LastSeen | summarize |
ServicePrincipalDisplayName | summarize |
ServicePrincipalObjectID | summarize |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
TI Map Domain entity to PaloAlto
#'Identifies a match in Palo Alto data in CommonSecurityLog table from any Domain IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 418192ba-01b8-4be8-89b7-5b5396a9d062
name: TI Map Domain entity to PaloAlto
description: |
'Identifies a match in Palo Alto data in CommonSecurityLog table from any Domain IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h; // Duration to look back for recent logs (1 hour)
let ioc_lookBack = 14d; // Duration to look back for recent threat intelligence indicators (14 days)
// Create a list of top-level domains (TLDs) in our threat feed for later validation of extracted domains
let list_tlds =
ThreatIntelIndicators
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend DomainName = tolower(DomainName)
| extend parts = split(DomainName, '.')
| extend tld = parts[(array_length(parts)-1)]
| summarize count() by tostring(tld)
| summarize make_list(tld);
let Domain_Indicators =
ThreatIntelIndicators
// Filter to pick up only IOC's that contain the entities we want (in this case, DomainName)
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend TI_DomainEntity = DomainName
| extend IndicatorId = tostring(split(Id, "--")[2]);
Domain_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type, TI_DomainEntity
// Join with CommonSecurityLog to find potential malicious activity
| join kind=innerunique (
CommonSecurityLog
| extend IngestionTime = ingestion_time()
| where IngestionTime > ago(dt_lookBack)
| where DeviceVendor =~ 'Palo Alto Networks'
| where DeviceEventClassID =~ 'url'
// Uncomment the line below to only alert on allowed connections
// | where DeviceAction !~ "block-url"
// Extract domain from RequestURL, if not present, extract it from AdditionalExtensions
| extend PA_Url = coalesce(RequestURL, "None")
| extend PA_Url = iif(isempty(PA_Url) and AdditionalExtensions !startswith "PanOS", extract("([^\"]+)", 1, tolower(AdditionalExtensions)), trim('"', PA_Url))
| extend PA_Url = iif(PA_Url !in~ ('None', 'http://None', 'https://None') and PA_Url !startswith "http://" and PA_Url !startswith "https://" and ApplicationProtocol !~ "ssl", strcat('http://', PA_Url), PA_Url)
| extend PA_Url = iif(PA_Url !in~ ('None', 'http://None', 'https://None') and PA_Url !startswith "https://" and ApplicationProtocol =~ "ssl", strcat('https://', PA_Url), PA_Url)
| extend Domain = trim(@"""", tostring(parse_url(PA_Url).Host))
| where isnotempty(Domain)
| extend Domain = tolower(Domain)
| extend parts = split(Domain, '.')
// Split out the top-level domain (TLD) for the purpose of checking if we have any TI indicators with this TLD to match on
| extend tld = parts[(array_length(parts)-1)]
// Validate parsed domain by checking TLD against TLDs from the threat feed and drop domains where there is no chance of a match
| where tld in~ (list_tlds)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
) on $left.TI_DomainEntity == $right.Domain
| where CommonSecurityLog_TimeGenerated < ValidUntil
// Group the results by IndicatorId and Domain and keep only the latest CommonSecurityLog_TimeGenerated
| summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by IndicatorId, Domain
// Select the desired fields for the final result set
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, PA_Url, Domain, IndicatorId, ValidUntil, Confidence, DeviceAction, DestinationIP, DestinationPort, DeviceName, SourceIP, SourcePort, ApplicationProtocol, RequestMethod, Type, TI_DomainEntity
// Add a new field 'timestamp' for convenience, using the CommonSecurityLog_TimeGenerated as its value
| extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: PA_Url
version: 1.4.5
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let Domain_Indicators is inlined into the numbered stages below.
Let Binding: list_tlds
let list_tlds = ThreatIntelIndicators
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend DomainName = tolower(DomainName)
| extend parts = split(DomainName, '.')
| extend tld = parts[(array_length(parts)-1)]
| summarize count() by tostring(tld)
| summarize make_list(tld);
Stages 1 to 10 define let Domain_Indicators as the rule's main pipeline source. Stages 11 to 18 use it.
Stage 1: source
ThreatIntelIndicators
Stage 2: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 3: where
| where IndicatorType == "domain-name"
Stage 4: extend
| extend DomainName = tolower(ObservableValue)
Stage 5: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 6: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 7: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 8: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 9: extend
| extend TI_DomainEntity = DomainName
Stage 10: extend
| extend IndicatorId = tostring(split(Id, "--")[2])
Stage 11: project-reorder
Domain_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type, TI_DomainEntity
Stage 12: join
| join kind=innerunique (
CommonSecurityLog
| extend IngestionTime = ingestion_time()
| where IngestionTime > ago(dt_lookBack)
| where DeviceVendor =~ 'Palo Alto Networks'
| where DeviceEventClassID =~ 'url'
| extend PA_Url = coalesce(RequestURL, "None")
| extend PA_Url = iif(isempty(PA_Url) and AdditionalExtensions !startswith "PanOS", extract("([^\"]+)", 1, tolower(AdditionalExtensions)), trim('"', PA_Url))
| extend PA_Url = iif(PA_Url !in~ ('None', 'http://None', 'https://None') and PA_Url !startswith "http://" and PA_Url !startswith "https://" and ApplicationProtocol !~ "ssl", strcat('http://', PA_Url), PA_Url)
| extend PA_Url = iif(PA_Url !in~ ('None', 'http://None', 'https://None') and PA_Url !startswith "https://" and ApplicationProtocol =~ "ssl", strcat('https://', PA_Url), PA_Url)
| extend Domain = trim(@"""", tostring(parse_url(PA_Url).Host))
| where isnotempty(Domain)
| extend Domain = tolower(Domain)
| extend parts = split(Domain, '.')
| extend tld = parts[(array_length(parts)-1)]
| where tld in~ (list_tlds)
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
) on $left.TI_DomainEntity == $right.Domain
Stage 13: where
where CommonSecurityLog_TimeGenerated < ValidUntil
Stage 14: summarize
summarize CommonSecurityLog_TimeGenerated by IndicatorId, Domain
Stage 15: extend
extend Description
Stage 16: extend
extend ActivityGroupNames
Stage 17: project
project ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, Confidence, Description, DestinationIP, DestinationPort, DeviceAction, DeviceName, Domain, IndicatorId, PA_Url, RequestMethod, SourceIP, SourcePort, TI_DomainEntity, Type, ValidUntil
Stage 18: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"url" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Palo Alto Networks" |
Domain | is_not_null | field:"Domain" kind:is_not_null | |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
tld | in |
| field:"tld" kind:in value:"list_tlds" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ApplicationProtocol | project |
CommonSecurityLog_TimeGenerated | project |
Confidence | project |
Description | project |
DestinationIP | project |
DestinationPort | project |
DeviceAction | project |
DeviceName | project |
Domain | project |
IndicatorId | project |
PA_Url | project |
RequestMethod | project |
SourceIP | project |
SourcePort | project |
TI_DomainEntity | project |
Type | project |
ValidUntil | project |
timestamp | extend |
UniFi Site Manager: Site Health Critical
#Identifies when a site has multiple offline devices, indicating significant site-level issues such as a power outage, network failure, or infrastructure problem.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: c8875ebb-cc6e-14e4-4216-d8b06fd92c90
name: "UniFi Site Manager: Site Health Critical"
description: |
Identifies when a site has multiple offline devices, indicating significant site-level issues such as a power outage, network failure, or infrastructure problem.
severity: High
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Sites_CL
queryFrequency: 15m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1489
- T1499
subTechniques:
- T1499.002
query: |
// UniFi Site Health Critical Detection
let OfflineThreshold = 3;
Unifi_SiteManager_Sites_CL
| where TimeGenerated > ago(30m)
| summarize arg_max(TimeGenerated, *) by SiteId
| where OfflineDevices >= OfflineThreshold
| extend
AvailabilityPct = round(100.0 * (TotalDevices - OfflineDevices) / TotalDevices, 1),
TotalClients = WifiClients + WiredClients
| project
TimeGenerated,
SiteId = SiteId,
SiteName = SiteName,
TotalDevices = TotalDevices,
OfflineDevices = OfflineDevices,
OnlineDevices = TotalDevices - OfflineDevices,
AvailabilityPct,
TotalClients,
WiFiClients = WifiClients,
WiredClients = WiredClients,
Timezone = Timezone
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SiteId
- identifier: DnsDomain
columnName: SiteName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: true
lookbackDuration: PT4H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Parameters
let OfflineThreshold = 3;
Stage 1: source
Unifi_SiteManager_Sites_CL
Stage 2: where
| where TimeGenerated > ago(30m)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by SiteId
Stage 4: where
| where OfflineDevices >= OfflineThreshold
Stage 5: extend
| extend
AvailabilityPct = round(100.0 * (TotalDevices - OfflineDevices) / TotalDevices, 1),
TotalClients = WifiClients + WiredClients
Stage 6: project
| project
TimeGenerated,
SiteId = SiteId,
SiteName = SiteName,
TotalDevices = TotalDevices,
OfflineDevices = OfflineDevices,
OnlineDevices = TotalDevices - OfflineDevices,
AvailabilityPct,
TotalClients,
WiFiClients = WifiClients,
WiredClients = WiredClients,
Timezone = Timezone
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OfflineDevices | ge |
| field:"OfflineDevices" kind:ge value:"3" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AvailabilityPct | project |
OfflineDevices | project |
OnlineDevices | project |
SiteId | project |
SiteName | project |
TimeGenerated | project |
Timezone | project |
TotalClients | project |
TotalDevices | project |
WiFiClients | project |
WiredClients | project |
Unusual Volume of Password Updated or Removed
#'This rule will check if there is an unnormal activity of sites that are deleted or changed per user. The normal amount of actions is calculated based on the previous 14 days of activity. If there is a significant increase, an incident will be created.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: a3bbdf60-0a6d-4cc2-b1d1-dd70aca184ce
name: Unusual Volume of Password Updated or Removed
description: |
'This rule will check if there is an unnormal activity of sites that are deleted or changed per user.
The normal amount of actions is calculated based on the previous 14 days of activity. If there is a significant increase, an incident will be created.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: LastPass
dataTypes:
- LastPassNativePoller_CL
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- Impact
relevantTechniques:
- T1485
query: |
let threshold = toscalar (LastPassNativePoller_CL
| where todatetime(Time_s) >= startofday(ago(14d)) and todatetime(Time_s) < startofday(ago(1d))
| where Action_s == "Site Changed" or Action_s == "Deleted Sites"
| summarize count() by Username_s, bin(todatetime(Time_s),1d)
| summarize avg(count_), stdev(count_)
| project threshold = avg_count_+stdev_count_*2);
LastPassNativePoller_CL
| where Username_s != "API"
| where Action_s == "Site Changed" or Action_s == "Deleted Sites" and todatetime(Time_s) >= startofday(ago(1d))
| summarize count() by Username_s, IP_Address_s
| where count_ > ['threshold']
| extend AccountCustomEntity = Username_s, IPCustomEntity = IP_Address_s
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Let Binding: threshold
let threshold = toscalar (LastPassNativePoller_CL
| where todatetime(Time_s) >= startofday(ago(14d)) and todatetime(Time_s) < startofday(ago(1d))
| where Action_s == "Site Changed" or Action_s == "Deleted Sites"
| summarize count() by Username_s, bin(todatetime(Time_s),1d)
| summarize avg(count_), stdev(count_)
| project threshold = avg_count_+stdev_count_*2);
Stage 1: source
LastPassNativePoller_CL
Stage 2: where
| where Username_s != "API"
Stage 3: where
| where Action_s == "Site Changed" or Action_s == "Deleted Sites" and todatetime(Time_s) >= startofday(ago(1d))
Stage 4: summarize
| summarize count() by Username_s, IP_Address_s
Stage 5: where
| where count_ > ['threshold']
Stage 6: extend
| extend AccountCustomEntity = Username_s, IPCustomEntity = IP_Address_s
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action_s | eq |
| field:"Action_s" kind:eq |
Username_s | ne |
| field:"Username_s" kind:ne value:"API" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IP_Address_s | summarize |
Username_s | summarize |
AccountCustomEntity | extend |
IPCustomEntity | extend |
UserAccountDisabled
#'This query searches for account is Disabled. Does not effect score as its easily enabled.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 24E0132F-61D1-41BD-9393-06136D1039C7
name: UserAccountDisabled
description: |
'This query searches for account is Disabled. Does not effect score as its easily enabled.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SenservaPro
dataTypes:
- SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
SenservaPro_CL
| where ControlName_s == 'UserDisabled'
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ControlName_s
- identifier: AadTenantId
columnName: TenantId
- identifier: DisplayName
columnName: TenantDisplayName_s
- entityType: SecurityGroup
fieldMappings:
- identifier: DistinguishedName
columnName: Group_s
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: SourceSystem
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SenservaPro_CL
Stage 2: where
| where ControlName_s == 'UserDisabled'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ControlName_s | eq |
| field:"ControlName_s" kind:eq value:"UserDisabled" |