Detection rules › Kusto

Kusto rules: 3h

RuleSeverity
[Entra ID] Privilege Elevation Request Deniedhigh
A host is potentially running a hacking tool (ASIM Web Session schema)medium
Account created from non-approved sourcesmedium
AWS Security Hub - Detect IAM Policies allowing full administrative privilegeshigh
Base64 encoded Windows process command-lines (Normalized Process Events)medium
BloodHound Attack Path Finding - Logons From Tier Zero Usersmedium
BloodHound Attack Path Finding - Ownership of Tier Zero Principalmedium
Cisco SE - Ransomware Activityhigh
Cloud Replica Permanent Failover Performed by Tenanthigh
CTE - Sudden Block Spike Detectedhigh
CYFIRMA - High severity Trojan Network Indicators - Block Recommended Rulehigh
CYFIRMA - Social and Public Exposure - Source Code Exposure on Public Repositories Rulemedium
Failed host logons but success logon to AzureADmedium
File Share Deletedhigh
Host Deletedlow
Mercury - Domain, Hash and IP IOCs - August 2022high
NordPass - User fails authenticationhigh
NRT Modified domain federation trust settingshigh
NRT Privileged Role Assigned Outside PIMlow
Pathlock TDnR - Transaction and Report Statisticsmedium
PRODAFT USTA - Non-expired payment card exposedhigh
Recorded Future Alerts Incident Creationmedium
SAP BTP - Cloud Identity Service application configuration monitormedium
SAP BTP - Cloud Integration package import or transportmedium
Service Principal Assigned App Role With Sensitive Accessmedium
TI Map Domain entity to PaloAltomedium
UniFi Site Manager: Site Health Criticalhigh
Unusual Volume of Password Updated or Removedlow
UserAccountDisabledmedium

[Entra ID] Privilege Elevation Request Denied

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

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
ActivityDisplayNameeq
  • Add member to role request denied (PIM activation) corpus 3 (kusto 3)
field:"azure_ad::activity_display_name" kind:eq value:"Add member to role request denied (PIM activation)"
InitiatedBy.useris_not_null
  • (no value, null check)
field:"InitiatedBy.user" kind:is_not_null
ResourceItem.typeeq
  • Role transforms: kusto_dynamic_path
  • User transforms: kusto_dynamic_path
field:"ResourceItem.type" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
Alert_Category_enproject
Alert_Containment_enproject
Alert_Description_enproject
Alert_Name_enproject
Alert_Remediation_enproject
Alert_SubCategory_enproject
Alert_Time_TWproject
Alert_Time_UTC0project
Alert_TriageStep_enproject
Event_Actionproject
Event_Descriptionproject
Event_Statusproject
Source_Identity_FullNameproject
Source_Identity_IDproject
Source_Identity_Typeproject
Source_Network_IPAddressproject
Source_Resource_IDproject
Source_Resource_Nameproject
Target_Identity_DomainTypeproject
Target_Identity_FullNameproject
Target_Identity_Typeproject
Target_Resource_IDproject
Target_Resource_Nameproject
Target_Resource_Typeproject

A host is potentially running a hacking tool (ASIM Web Session schema)

#
Severity
medium
Time window
15m
Author
Yaron
Source
github.com/Azure/Azure-Sentinel

'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

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.

FieldSource
HttpUserAgentproject
SrcIpAddrproject
SrcUsernameproject
TimeGeneratedproject
Urlproject
AccountNameextend
AccountUPNSuffixextend

Account created from non-approved sources

#
Severity
medium
Time window
7d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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

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

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 =
ifInitiatingUserPrincipalName has "#EXT#"tostring(split(tostring(split(InitiatingUserPrincipalName, "#EXT#")[0]), "_")[0])
elifnot (InitiatingUserPrincipalName has "#EXT#")tostring(split(InitiatingUserPrincipalName, "@")[0])
elseInitiatingUserPrincipalName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Add User corpus 4 (kusto 4)
field:"OperationName" kind:eq value:"Add User"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserAddedDomaineqalternative_domainsexcludes:UserAddedDomain field:"UserAddedDomain" value:"alternative_domains"
UserAddedDomaineqcore_domainsexcludes:UserAddedDomain field:"UserAddedDomain" value:"core_domains"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
UserAddedextend
UserAddedDomainextend
AddedByNameextend
AddedByUPNSuffixextend
UserAddedNameextend

AWS Security Hub - Detect IAM Policies allowing full administrative privileges

#
Status
available
Severity
high
Time window
1h
Group by
AwsAccountId, AwsRegion, AwsSecurityFindingDescription, AwsSecurityFindingId, AwsSecurityFindingTitle, ComplianceSecurityControlId, IAMPolicyDescription, IAMPolicyId, IAMPolicyName
Source
github.com/Azure/Azure-Sentinel

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.

FieldSource
AwsAccountIdsummarize
AwsRegionsummarize
AwsSecurityFindingDescriptionsummarize
AwsSecurityFindingIdsummarize
AwsSecurityFindingTitlesummarize
ComplianceSecurityControlIdsummarize
IAMPolicyDescriptionsummarize
IAMPolicyIdsummarize
IAMPolicyNamesummarize
TimeGeneratedsummarize

Base64 encoded Windows process command-lines (Normalized Process Events)

#
Severity
medium
Time window
1d
Group by
ActingProcessName, ActorUsername, CommandLine, Dvc, EventProduct, EventVendor, Process
Author
Yuval Naor
Source
github.com/Azure/Azure-Sentinel

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

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.

FieldKindValuesSearch
CommandLinecontains
  • TVqQAAMAAAAEAAA corpus 2 (kusto 2)
field:"CommandLine" kind:contains value:"TVqQAAMAAAAEAAA"
Processis_not_null
  • (no value, null check)
field:"Process" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActingProcessNamesummarize
ActorUsernamesummarize
CommandLinesummarize
Dvcsummarize
EndTimesummarize
EventProductsummarize
EventVendorsummarize
Processsummarize
StartTimesummarize
AccountNTDomainextend
AccountNameextend
HostNameextend
HostNameDomainextend

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.

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

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.

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.

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

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.

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

Cisco SE - Ransomware Activity

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

'This rule is triggered when possible ransomware activity is detected on host.'

MITRE ATT&CK Coverage

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

FieldKindValuesSearch
EventMessagematch
  • Suspected ransomware transforms: term
field:"EventMessage" kind:match value:"Suspected ransomware"

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
MalwareCustomEntityextend

Cloud Replica Permanent Failover Performed by Tenant

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Data SourceIDproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Replica Nameproject
Severityproject
Tenant IDproject

CTE - Sudden Block Spike Detected

#
Severity
high
Time window
10m
Group by
DeviceId, DeviceOs, DeviceUser, HostName, TenantId
Source
github.com/Azure/Azure-Sentinel

A sudden spike in blocked URLs/domains on a single device may indicate an active phishing or malware campaign.

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifisnotempty(tostring(d.hostname))tostring(d.hostname)
elsestrcat("cte-device-", substring(EntityId, 0, 8))
RuleName =
ifisnotempty(tostring(d.ruleName))tostring(d.ruleName)
else"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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
BlockCountsummarize
DeviceIdsummarize
DeviceOssummarize
DeviceUsersummarize
HostNamesummarize
LastHitsummarize
SampleIpssummarize
SampleUrlssummarize
TenantIdsummarize
ClientIpextend
Urlextend

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.

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

"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

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.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
patterncontainsfile:hashesexcludes:pattern field:"pattern" value:"file:hashes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConfidenceScoreproject
Countryproject
Descriptionproject
Domainproject
IPAbuseproject
IPv4project
IPv6project
IndicatorIDproject
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
URLproject
createdproject
modifiedproject
nameproject
valid_fromproject

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.

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

"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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertTitleproject
AlertUIDproject
AssetTypeproject
AssetValueproject
Descriptionproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
RiskScoreproject
TimeGeneratedproject
UIDproject

Failed host logons but success logon to AzureAD

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

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

TacticTechniques
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

File Share Deleted

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

Detects when a file share is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: 7ddaf8ef-b726-408b-b1a7-a7a4f92cbf6d
name: File Share Deleted
description: Detects when a file share is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
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 == 28920\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\
  \    UserName = 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 == 28920

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,
    UserName = user,
    ["Data SourceName"] = Name,
    MessageDetails = Description,
    Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Data SourceNameproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Host Deleted

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Data SourceNameproject
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Mercury - Domain, Hash and IP IOCs - August 2022

#
Severity
high
Time window
12h
Author
Ajeet Prakash
Source
github.com/Azure/Azure-Sentinel

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

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

FieldKindValuesSearch
Categoryeq
  • AzureFirewallApplicationRule corpus 3 (kusto 3)
  • AzureFirewallDnsProxy corpus 2 (kusto 2)
  • AzureFirewallNetworkRule
field:"Category" kind:eq
ClientIPin
  • IPList
field:"ClientIP" kind:in value:"IPList"
DestinationHostis_not_null
  • (no value, null check)
field:"DestinationHost" kind:is_not_null
DestinationHostmatch
  • IPList transforms: term
  • domains transforms: term
field:"DestinationHost" kind:match
DestinationHostNamematch
  • domains transforms: term
field:"DestinationHostName" kind:match value:"domains"
DestinationIPin
  • IPList
field:"DestinationIP" kind:in value:"IPList"
DestinationIpin
  • IPList
field:"dest_ip" kind:in value:"IPList"
DestinationIpmatch
  • IPList transforms: term
field:"dest_ip" kind:match value:"IPList"
EventIDeq
  • 1 corpus 12 (kusto 11, elastic 1)
  • 3 corpus 5 (kusto 3, chronicle 2)
field:"EventID" kind:eq
FileHashin
  • sha256Hashes
field:"FileHash" kind:in value:"sha256Hashes"
Fqdnmatch
  • IPList transforms: term
  • domains transforms: term
field:"Fqdn" kind:match
Hashes[0]eq
  • SHA256 transforms: kusto_dynamic_path
field:"Hashes[0]" kind:eq value:"SHA256"
Hashes[1]match
  • sha256Hashes transforms: term, kusto_dynamic_path
field:"Hashes[1]" kind:match value:"sha256Hashes"
IPAddressesin
  • IPList
field:"IPAddresses" kind:in value:"IPList"
InitiatingProcessSHA256in
  • sha256Hashes
field:"InitiatingProcessSHA256" kind:in value:"sha256Hashes"
Messagematch
  • IPList transforms: term
field:"Message" kind:match value:"IPList"
Namein
  • domains
field:"Name" kind:in value:"domains"
QueryNameis_not_null
  • (no value, null check)
field:"QueryName" kind:is_not_null
QueryNamematch
  • domains transforms: term
field:"QueryName" kind:match value:"domains"
RemoteDnsCanonicalNamesmatch
  • domains transforms: term
field:"RemoteDnsCanonicalNames" kind:match value:"domains"
RemoteIPin
  • IPList
field:"DestinationIp" kind:in value:"IPList"
RemoteUrlmatch
  • domains transforms: term
field:"DestinationHostname" kind:match value:"domains"
RequestURLmatch
  • domains transforms: term
field:"RequestURL" kind:match value:"domains"
ResourceTypeeq
  • AZUREFIREWALLS corpus 3 (kusto 3)
field:"ResourceType" kind:eq value:"AZUREFIREWALLS"
SHA256match
  • sha256Hashes transforms: term
field:"sha256" kind:match value:"sha256Hashes"
SourceIPin
  • IPList
field:"SourceIP" kind:in value:"IPList"
SourceIpin
  • IPList
field:"src_ip" kind:in value:"IPList"
TargetFileSHA256match
  • sha256Hashes transforms: term
field:"sha256" kind:match value:"sha256Hashes"
msg_smatch
  • IPList transforms: term
  • domains transforms: term
field:"msg_s" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNTproject
CommandLineproject
Computerproject
EventDetailproject
Hashesproject
Imageproject
Sourceproject
TimeGeneratedproject
Typeextend
Algorithmextend
FileHashextend
AccountNTDomainextend
AccountNTNameextend
HostNameextend
HostNameDomainextend

NordPass - User fails authentication

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

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

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPAddressextend
Providerextend
TargetEmailextend

NRT Modified domain federation trust settings

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

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

TacticTechniques
Persistence
Privilege Escalation
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
modifiedPropertiesextend
Federatedextend
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
Nameextend
UPNSuffixextend

NRT Privileged Role Assigned Outside PIM

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

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

TacticTechniques
Privilege Escalation

Telemetry Coverage

PlatformRecord / event type
AzureAdd member to role

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
Identityne
  • MS-PIM corpus 3 (kusto 3)
  • MS-PIM-Fairfax corpus 3 (kusto 3)
field:"Identity" kind:ne
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
field:"azure_ad::logged_by_service" kind:eq value:"Core Directory"
OperationNameeq
  • Add member to role corpus 2 (kusto 2)
field:"OperationName" kind:eq value:"Add member to role"
OperationNamematch
  • Add member to role outside of PIM transforms: term corpus 2 (kusto 2)
field:"OperationName" kind:match value:"Add member to role outside of PIM"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
TargetNameextend
TargetUPNSuffixextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

Pathlock TDnR - Transaction and Report Statistics

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

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

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

PRODAFT USTA - Non-expired payment card exposed

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

'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

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

FieldSource
CardBinproject
CardBrandproject
CardLast4project
CardLengthproject
CardMaskedproject
CompanyNameproject
Createdproject
ExpirationDateproject
Statusproject
TicketIdproject
TimeGeneratedproject

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.

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

'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

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

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

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 =
ifaction == "create"strcat("Application '", serviceProviderName, "' (", federationType, ") was created")
elifaction == "update"strcat("Application '", serviceProviderName, "' (", federationType, ") was updated")
elifaction == "delete"strcat("Application '", serviceProviderName, "' was deleted")
else"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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
CloudAppproject
FederationTypeproject
MessageTextproject
ServiceProviderNameproject
Tenantproject
UpdatedOnproject
UserNameproject
ipAddressproject
AccountNameextend
UPNSuffixextend

SAP BTP - Cloud Integration package import or transport

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

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

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

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 =
ifaction endswith "Started""Started"
elifaction endswith "Completed""Completed"
else"Unknown"
operationType =
ifaction contains "Import""Import"
elifaction contains "Transport""Transport"
else"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.

FieldKindValuesSearch
Categoryeq
  • audit.security-events
field:"Category" kind:eq value:"audit.security-events"
actionin
  • Artifact_Transport_Completed
  • Artifact_Transport_Started
  • Package_Import_Completed
  • Package_Import_Started
  • Package_Transport_Completed
  • Package_Transport_Started
field:"action" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
CloudAppproject
MessageTextproject
ObjectIdproject
ObjectTypeproject
OperationStatusproject
OperationTypeproject
Tenantproject
UpdatedOnproject
UserNameproject
ipAddressproject
AccountNameextend
UPNSuffixextend

Service Principal Assigned App Role With Sensitive Access

#
Severity
medium
Time window
1d
Group by
AddedPermissions, CorrelationId, Id, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, ServicePrincipalDisplayName, ServicePrincipalObjectID
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 =
ifdisplayName == "ServicePrincipal.ObjectID""ServicePrincipalObjectID"
elifdisplayName == "ServicePrincipal.DisplayName""ServicePrincipalDisplayName"
elsedisplayName

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

FieldKindValuesSearch
OperationNameeq
  • Add app role assignment to service principal corpus 3 (kusto 3)
field:"OperationName" kind:eq value:"Add app role assignment to service principal"
Permissionsmatch
  • .All transforms: term
  • ChannelMessage.Read transforms: term
  • Chat.Read transforms: term
  • EAS.AccessAsUser.All transforms: term
  • EWS.AccessAsUser.All transforms: term
  • Files.Read transforms: term
  • Group.Read transforms: term
  • Mail. transforms: term
  • Notes.Read transforms: term
  • ReadWrite transforms: term
  • TeamsActivity.Read transforms: term
  • offline_access transforms: term
field:"Permissions" kind:match
TargetResources_0_modifiedPropertiesis_not_null
  • (no value, null check)
field:"TargetResources_0_modifiedProperties" kind:is_not_null
TargetResources_0_modifiedProperties.displayNameeq
  • AppRole.Value transforms: kusto_dynamic_path
  • DelegatedPermissionGrant.Scope transforms: kusto_dynamic_path
field:"TargetResources_0_modifiedProperties.displayName" kind:eq
displayNameeq
  • ServicePrincipal.DisplayName
  • ServicePrincipal.ObjectID
field:"displayName" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AddedPermissionssummarize
CorrelationIdsummarize
EventIdssummarize
FirstSeensummarize
InitiatedBysummarize
InitiatingAadUserIdsummarize
InitiatingAppNamesummarize
InitiatingAppServicePrincipalIdsummarize
InitiatingIPAddresssummarize
InitiatingUserPrincipalNamesummarize
LastSeensummarize
ServicePrincipalDisplayNamesummarize
ServicePrincipalObjectIDsummarize
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

TI Map Domain entity to PaloAlto

#
Severity
medium
Time window
14d
Group by
$left.TI_DomainEntity, $right.Domain, Domain, Id, IndicatorId, ObservableValue
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in Palo Alto data in CommonSecurityLog table from any Domain IOC from TI'

MITRE ATT&CK Coverage

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ApplicationProtocolproject
CommonSecurityLog_TimeGeneratedproject
Confidenceproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DeviceActionproject
DeviceNameproject
Domainproject
IndicatorIdproject
PA_Urlproject
RequestMethodproject
SourceIPproject
SourcePortproject
TI_DomainEntityproject
Typeproject
ValidUntilproject
timestampextend

UniFi Site Manager: Site Health Critical

#
Status
available
Severity
high
Time window
30m
Group by
SiteId
Source
github.com/Azure/Azure-Sentinel

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

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.

FieldKindValuesSearch
OfflineDevicesge
  • 3 transforms: number
field:"OfflineDevices" kind:ge value:"3"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvailabilityPctproject
OfflineDevicesproject
OnlineDevicesproject
SiteIdproject
SiteNameproject
TimeGeneratedproject
Timezoneproject
TotalClientsproject
TotalDevicesproject
WiFiClientsproject
WiredClientsproject

Unusual Volume of Password Updated or Removed

#
Status
available
Severity
low
Time window
14d
Group by
IP_Address_s, Username_s
Source
github.com/Azure/Azure-Sentinel

'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

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IP_Address_ssummarize
Username_ssummarize
AccountCustomEntityextend
IPCustomEntityextend

UserAccountDisabled

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

'This query searches for account is Disabled. Does not effect score as its easily enabled.'

MITRE ATT&CK Coverage

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

Back to all Kusto rules