Detection rules › Kusto

Kusto rules: 1o

Admin promotion after Role Management Application Permission Grant

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

This rule looks for a service principal being granted the Microsoft Graph RoleManagement.ReadWrite.Directory (application) permission before being used to add an Microsoft Entra ID object or user account to an Admin directory role (i.e. Global Administrators). This is a known attack path that is usually abused when a service principal already has the AppRoleAssignment.ReadWrite.All permission granted. This permission allows an app to manage permission grants for application permissions to any API. A service principal can promote itself or other service principals to admin roles (i.e. Global Administrators). This would be considered a privilege escalation technique. Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions, https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: f80d951a-eddc-4171-b9d0-d616bb83efdc
name: Admin promotion after Role Management Application Permission Grant
description: |
  'This rule looks for a service principal being granted the Microsoft Graph RoleManagement.ReadWrite.Directory (application) permission before being used to add an Microsoft Entra ID object or user account to an Admin directory role (i.e. Global Administrators).
  This is a known attack path that is usually abused when a service principal already has the AppRoleAssignment.ReadWrite.All permission granted. This permission allows an app to manage permission grants for application permissions to any API.
  A service principal can promote itself or other service principals to admin roles (i.e. Global Administrators). This would be considered a privilege escalation technique.
  Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions, https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1098.003
  - T1078.004
tags:
  - SimuLand
query: |
  let query_frequency = 1h;
  let query_period = 2h;
  AuditLogs
  | where TimeGenerated > ago(query_period)
  | where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory"
  | where OperationName =~ "Add app role assignment to service principal"
  | mv-expand TargetResource = TargetResources
  | mv-expand modifiedProperty = TargetResource["modifiedProperties"]
  | where tostring(modifiedProperty["displayName"]) == "AppRole.Value"
  | extend PermissionGrant = tostring(modifiedProperty["newValue"])
  | where PermissionGrant has "RoleManagement.ReadWrite.Directory"
  | mv-apply modifiedProperty = TargetResource["modifiedProperties"] on (
      summarize modifiedProperties = make_bag(
          bag_pack(tostring(modifiedProperty["displayName"]),
              bag_pack("oldValue", trim(@'[\"\s]+', tostring(modifiedProperty["oldValue"])),
                  "newValue", trim(@'[\"\s]+', tostring(modifiedProperty["newValue"])))), 100)
  )
  | project
      PermissionGrant_TimeGenerated = TimeGenerated,
      PermissionGrant_OperationName = OperationName,
      PermissionGrant_Result = Result,
      PermissionGrant,
      AppDisplayName = tostring(modifiedProperties["ServicePrincipal.DisplayName"]["newValue"]),
      AppServicePrincipalId = tostring(modifiedProperties["ServicePrincipal.ObjectID"]["newValue"]),
      PermissionGrant_InitiatedBy = InitiatedBy,
      PermissionGrant_TargetResources = TargetResources,
      PermissionGrant_AdditionalDetails = AdditionalDetails,
      PermissionGrant_CorrelationId = CorrelationId
  | join kind=inner (
      AuditLogs
      | where TimeGenerated > ago(query_frequency)
      | where Category =~ "RoleManagement" and LoggedByService =~ "Core Directory" and AADOperationType =~ "Assign"
      | where isnotempty(InitiatedBy["app"])
      | mv-expand TargetResource = TargetResources
      | mv-expand modifiedProperty = TargetResource["modifiedProperties"]
      | where tostring(modifiedProperty["displayName"]) in ("Role.DisplayName", "RoleDefinition.DisplayName")
      | extend RoleAssignment = tostring(modifiedProperty["newValue"])
      | where RoleAssignment contains "Admin"
      | project
          RoleAssignment_TimeGenerated = TimeGenerated,
          RoleAssignment_OperationName = OperationName,
          RoleAssignment_Result = Result,
          RoleAssignment,
          TargetType = tostring(TargetResources[0]["type"]),
          Target = iff(isnotempty(TargetResources[0]["displayName"]), tostring(TargetResources[0]["displayName"]), tolower(TargetResources[0]["userPrincipalName"])),
          TargetId = tostring(TargetResources[0]["id"]),
          RoleAssignment_InitiatedBy = InitiatedBy,
          RoleAssignment_TargetResources = TargetResources,
          RoleAssignment_AdditionalDetails = AdditionalDetails,
          RoleAssignment_CorrelationId = CorrelationId,
          AppServicePrincipalId = tostring(InitiatedBy["app"]["servicePrincipalId"])
      ) on AppServicePrincipalId
  | where PermissionGrant_TimeGenerated < RoleAssignment_TimeGenerated
  | extend
      TargetName = tostring(split(Target, "@")[0]),
      TargetUPNSuffix = tostring(split(Target, "@")[1])
  | project PermissionGrant_TimeGenerated, PermissionGrant_OperationName, PermissionGrant_Result, PermissionGrant, AppDisplayName, AppServicePrincipalId, PermissionGrant_InitiatedBy, PermissionGrant_TargetResources, PermissionGrant_AdditionalDetails, PermissionGrant_CorrelationId, 
  RoleAssignment_TimeGenerated, RoleAssignment_OperationName, RoleAssignment_Result, RoleAssignment, TargetType, Target, TargetName, TargetUPNSuffix, TargetId, RoleAssignment_InitiatedBy, RoleAssignment_TargetResources, RoleAssignment_AdditionalDetails, RoleAssignment_CorrelationId
  | extend PermissionGrant_InitiatingUserPrincipalName = tostring(PermissionGrant_InitiatedBy.user.userPrincipalName)
  | extend PermissionGrant_InitiatingAadUserId = tostring(PermissionGrant_InitiatedBy.user.id)
  | extend PermissionGrant_InitiatingIpAddress = tostring(iff(isnotempty(PermissionGrant_InitiatedBy.user.ipAddress), PermissionGrant_InitiatedBy.user.ipAddress, PermissionGrant_InitiatedBy.app.ipAddress))
  | extend PermissionGrant_InitiatingAccountName = tostring(split(PermissionGrant_InitiatingUserPrincipalName, "@")[0]), PermissionGrant_InitiatingAccountUPNSuffix = tostring(split(PermissionGrant_InitiatingUserPrincipalName, "@")[1])
  | extend RoleAssignment_InitiatingUserPrincipalName = tostring(RoleAssignment_InitiatedBy.user.userPrincipalName)
  | extend RoleAssignment_InitiatingAadUserId = tostring(RoleAssignment_InitiatedBy.user.id)
  | extend RoleAssignment_InitiatingIpAddress = tostring(iff(isnotempty(RoleAssignment_InitiatedBy.user.ipAddress), RoleAssignment_InitiatedBy.user.ipAddress, RoleAssignment_InitiatedBy.app.ipAddress))
  | extend RoleAssignment_InitiatingAccountName = tostring(split(RoleAssignment_InitiatingUserPrincipalName, "@")[0]),  RoleAssignment_InitiatingAccountUPNSuffix = tostring(split(RoleAssignment_InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AppDisplayName
      - identifier: AadUserId
        columnName: AppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PermissionGrant_InitiatingUserPrincipalName
      - identifier: Name
        columnName: PermissionGrant_InitiatingAccountName
      - identifier: UPNSuffix
        columnName: PermissionGrant_InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: PermissionGrant_InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RoleAssignment_InitiatingUserPrincipalName
      - identifier: Name
        columnName: RoleAssignment_InitiatingAccountName
      - identifier: UPNSuffix
        columnName: RoleAssignment_InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: RoleAssignment_InitiatingAadUserId
version: 1.1.0
kind: Scheduled

Stages and Predicates

Parameters

let query_frequency = 1h;
let query_period = 2h;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(query_period)

Stage 3: where

| where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory"

Stage 4: where

| where OperationName =~ "Add app role assignment to service principal"

Stage 5: mv-expand

| mv-expand TargetResource = TargetResources

Stage 6: mv-expand

| mv-expand modifiedProperty = TargetResource["modifiedProperties"]

Stage 7: where

| where tostring(modifiedProperty["displayName"]) == "AppRole.Value"

Stage 8: extend

| extend PermissionGrant = tostring(modifiedProperty["newValue"])

Stage 9: where

| where PermissionGrant has "RoleManagement.ReadWrite.Directory"

Stage 10: kusto:mv-apply

| mv-apply modifiedProperty = TargetResource["modifiedProperties"] on (
    summarize modifiedProperties = make_bag(
        bag_pack(tostring(modifiedProperty["displayName"]),
            bag_pack("oldValue", trim(@'[\"\s]+', tostring(modifiedProperty["oldValue"])),
                "newValue", trim(@'[\"\s]+', tostring(modifiedProperty["newValue"])))), 100)
)

Stage 11: project

| project
    PermissionGrant_TimeGenerated = TimeGenerated,
    PermissionGrant_OperationName = OperationName,
    PermissionGrant_Result = Result,
    PermissionGrant,
    AppDisplayName = tostring(modifiedProperties["ServicePrincipal.DisplayName"]["newValue"]),
    AppServicePrincipalId = tostring(modifiedProperties["ServicePrincipal.ObjectID"]["newValue"]),
    PermissionGrant_InitiatedBy = InitiatedBy,
    PermissionGrant_TargetResources = TargetResources,
    PermissionGrant_AdditionalDetails = AdditionalDetails,
    PermissionGrant_CorrelationId = CorrelationId

Stage 12: join

| join kind=inner (
    AuditLogs
    | where TimeGenerated > ago(query_frequency)
    | where Category =~ "RoleManagement" and LoggedByService =~ "Core Directory" and AADOperationType =~ "Assign"
    | where isnotempty(InitiatedBy["app"])
    | mv-expand TargetResource = TargetResources
    | mv-expand modifiedProperty = TargetResource["modifiedProperties"]
    | where tostring(modifiedProperty["displayName"]) in ("Role.DisplayName", "RoleDefinition.DisplayName")
    | extend RoleAssignment = tostring(modifiedProperty["newValue"])
    | where RoleAssignment contains "Admin"
    | project
        RoleAssignment_TimeGenerated = TimeGenerated,
        RoleAssignment_OperationName = OperationName,
        RoleAssignment_Result = Result,
        RoleAssignment,
        TargetType = tostring(TargetResources[0]["type"]),
        Target = iff(isnotempty(TargetResources[0]["displayName"]), tostring(TargetResources[0]["displayName"]), tolower(TargetResources[0]["userPrincipalName"])),
        TargetId = tostring(TargetResources[0]["id"]),
        RoleAssignment_InitiatedBy = InitiatedBy,
        RoleAssignment_TargetResources = TargetResources,
        RoleAssignment_AdditionalDetails = AdditionalDetails,
        RoleAssignment_CorrelationId = CorrelationId,
        AppServicePrincipalId = tostring(InitiatedBy["app"]["servicePrincipalId"])
    ) on AppServicePrincipalId

Stage 13: where

where PermissionGrant_TimeGenerated < RoleAssignment_TimeGenerated

Stage 14: extend

extend TargetName, TargetUPNSuffix

Stage 15: project

project AppDisplayName, AppServicePrincipalId, PermissionGrant, PermissionGrant_AdditionalDetails, PermissionGrant_CorrelationId, PermissionGrant_InitiatedBy, PermissionGrant_OperationName, PermissionGrant_Result, PermissionGrant_TargetResources, PermissionGrant_TimeGenerated, RoleAssignment, RoleAssignment_AdditionalDetails, RoleAssignment_CorrelationId, RoleAssignment_InitiatedBy, RoleAssignment_OperationName, RoleAssignment_Result, RoleAssignment_TargetResources, RoleAssignment_TimeGenerated, Target, TargetId, TargetName, TargetType, TargetUPNSuffix

Stage 16: extend (8 consecutive steps)

extend PermissionGrant_InitiatingAadUserId, PermissionGrant_InitiatingAccountName, PermissionGrant_InitiatingAccountUPNSuffix, PermissionGrant_InitiatingIpAddress, PermissionGrant_InitiatingUserPrincipalName, RoleAssignment_InitiatingAadUserId, RoleAssignment_InitiatingAccountName, RoleAssignment_InitiatingAccountUPNSuffix, RoleAssignment_InitiatingIpAddress, RoleAssignment_InitiatingUserPrincipalName

Stage 17: summarize aggregation inside the mv-apply branch

summarize

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypeeq
  • Assign
field:"AADOperationType" kind:eq value:"Assign"
Categoryeq
  • ApplicationManagement corpus 13 (kusto 13)
  • RoleManagement
field:"Category" kind:eq
InitiatedBy["app"]is_not_null
  • (no value, null check)
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
  • Core Directory
field:"azure_ad::logged_by_service" kind:eq
OperationNameeq
  • Add app role assignment to service principal corpus 3 (kusto 3)
field:"OperationName" kind:eq value:"Add app role assignment to service principal"
PermissionGrantmatch
  • RoleManagement.ReadWrite.Directory transforms: term
field:"PermissionGrant" kind:match value:"RoleManagement.ReadWrite.Directory"
RoleAssignmentcontains
  • Admin
field:"RoleAssignment" kind:contains value:"Admin"
modifiedProperty["displayName"]eq
  • AppRole.Value transforms: tostring, kusto_dynamic_path
modifiedProperty["displayName"]in
  • Role.DisplayName transforms: tostring, kusto_dynamic_path
  • RoleDefinition.DisplayName transforms: tostring, kusto_dynamic_path

CYFIRMA - Attack Surface - Domain/IP Vulnerability Exposure High 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
high
Time window
5m
Group by
uid
Source
github.com/Azure/Azure-Sentinel

"This rule is triggered when CYFIRMA identifies publicly exposed vulnerabilities on domains or IP addresses within your organization's attack surface. These vulnerabilities may include outdated software, missing patches, insecure services, or misconfigurations that can be exploited by threat actors.Such exposure significantly increases the risk of exploitation, lateral movement, or data breach. Immediate investigation and remediation are recommended."

MITRE ATT&CK Coverage

Rule Body

id: fbe4f5e0-d93e-4c93-8cf9-925eb8ea7f2e
name: CYFIRMA - Attack Surface - Domain/IP Vulnerability Exposure High Rule
description: |
  "This rule is triggered when CYFIRMA identifies publicly exposed vulnerabilities on domains or IP addresses within your organization's attack surface. These vulnerabilities may include outdated software, missing patches, insecure services, or misconfigurations that can be exploited by threat actors.Such exposure significantly increases the risk of exploitation, lateral movement, or data breach. Immediate investigation and remediation are recommended."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaAttackSurfaceAlertsConnector
    dataTypes:
      - CyfirmaASDomainIPVulnerabilityAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Discovery
  - DefenseEvasion
  - Persistence
  - Execution
  - Impact
  - PrivilegeEscalation
relevantTechniques:
  - T1505
  - T1068
  - T1046
  - T1499
query: |
  // High Severity Domain/IP Vulnerability Exposure Detected
  let timeFrame = 5m;
  CyfirmaASDomainIPVulnerabilityAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | mv-expand pvuln = possible_vulnerabilities
  | extend cve = tostring(pvuln.cveNo), vulProducts = pvuln.products
  | mv-expand vul_Products = vulProducts
  | summarize cveList = make_set(cve, 100), vul_products1 = make_set(vul_Products, 100)    by uid
  | join kind=inner (CyfirmaASDomainIPVulnerabilityAlerts_CL
      | where severity == 'Critical'
      | summarize arg_max(TimeGenerated, *) by uid)
      on uid
  | extend
      Vulnerabilities = strcat_array(cveList, ', '),
      VulnerabilityProducts = strcat_array(vul_products1, ', '),
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      Domain=sub_domain,
      TopDomain=top_domain,
      NetworkIP=ip,
      AlertUID=alert_uid,
      UID=uid,
      OpenPorts=open_ports,
      HostProvider=host_provider,
      Country=country,
      Softwares=softwares,
      WebServer=web_server,
      WebServerVersion=web_server_version,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      Domain,
      TopDomain,
      RiskScore,
      FirstSeen,
      LastSeen,
      NetworkIP,
      AlertUID,
      UID,
      Vulnerabilities,
      VulnerabilityProducts,
      OpenPorts,
      HostProvider,
      Country,
      Softwares,
      WebServer,
      WebServerVersion,
      ProviderName,
      ProductName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: TopDomain
      - identifier: DnsDomain
        columnName: Domain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: NetworkIP
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  Vulnerabilities: Vulnerabilities
  UID: UID
  OpenPorts: OpenPorts
  WebServer: WebServer
  WebServerVersion: WebServerVersion
  Softwares: Softwares
  HostProvider: HostProvider
  AlertUID: AlertUID
  Country: Country
  vulnerableProducts: VulnerabilityProducts
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Domain/IP Vulnerability Exposure Detected - Domain: {{Domain}}, IP: {{NetworkIP}}"
  alertDescriptionFormat: "CYFIRMA - High Severity Domain/IP Vulnerability Exposure Detected - {{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

CyfirmaASDomainIPVulnerabilityAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: mv-expand

| mv-expand pvuln = possible_vulnerabilities

Stage 4: extend

| extend cve = tostring(pvuln.cveNo), vulProducts = pvuln.products

Stage 5: mv-expand

| mv-expand vul_Products = vulProducts

Stage 6: summarize

| summarize cveList = make_set(cve, 100), vul_products1 = make_set(vul_Products, 100)    by uid

Stage 7: join

| join kind=inner (CyfirmaASDomainIPVulnerabilityAlerts_CL
    | where severity == 'Critical'
    | summarize arg_max(TimeGenerated, *) by uid)
    on uid

Stage 8: extend

extend AlertUID, Country, Description, Domain, FirstSeen, HostProvider, LastSeen, NetworkIP, OpenPorts, ProductName, ProviderName, RiskScore, Softwares, TopDomain, UID, Vulnerabilities, VulnerabilityProducts, WebServer, WebServerVersion

Stage 9: project

project AlertUID, Country, Description, Domain, FirstSeen, HostProvider, LastSeen, NetworkIP, OpenPorts, ProductName, ProviderName, RiskScore, Softwares, TimeGenerated, TopDomain, UID, Vulnerabilities, VulnerabilityProducts, WebServer, WebServerVersion

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
Countryproject
Descriptionproject
Domainproject
FirstSeenproject
HostProviderproject
LastSeenproject
NetworkIPproject
OpenPortsproject
ProductNameproject
ProviderNameproject
RiskScoreproject
Softwaresproject
TimeGeneratedproject
TopDomainproject
UIDproject
Vulnerabilitiesproject
VulnerabilityProductsproject
WebServerproject
WebServerVersionproject

CYFIRMA - Compromised Employees Detection 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
high
Time window
5m
Group by
uid
Source
github.com/Azure/Azure-Sentinel

"Identifies and alerts on internal employee accounts that have been compromised, based on CYFIRMA's threat intelligence. This rule captures the latest exposure of user credentials, IP addresses, hostnames, operating systems, and pass hashes observed in the threat feed. It supports rapid detection and investigation of phishing, stealer malware, and insider compromise scenarios."

MITRE ATT&CK Coverage

Rule Body

id: 72d3fb86-d1eb-44d6-9352-170c6bb45bb7
name: CYFIRMA - Compromised Employees Detection Rule
description: |
  "Identifies and alerts on internal employee accounts that have been compromised, based on CYFIRMA's threat intelligence.
  This rule captures the latest exposure of user credentials, IP addresses, hostnames, operating systems, and pass hashes observed in the threat feed.
  It supports rapid detection and investigation of phishing, stealer malware, and insider compromise scenarios."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaCompromisedAccountsDataConnector
    dataTypes:
      - CyfirmaCompromisedAccounts_CL
queryFrequency: 5m
queryPeriod: 5m
suppressionDuration: 6h
suppressionEnabled: true
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1003
  - T1552
  - T1078
  - T1098
query: |
  // Compromised Employees - Latest per UID
  let timeFrame = 5m;
  CyfirmaCompromisedAccounts_CL
  | where TimeGenerated between (ago(timeFrame) .. now())
      and Category has "Compromised Employees"
  | extend 
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | summarize arg_max(TimeGenerated, 
      url,
      ip,
      email,
      user_name,
      computer_name,
      operating_system,
      breach_date,
      first_seen,
      last_seen,
      impact,
      recommendations,
      description,
      source,
      pass_hash,
      ProductName,
      ProviderName
  ) by uid
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: user_name
      - identifier: UPNSuffix
        columnName: email
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: computer_name
      - identifier: OSVersion
        columnName: operating_system
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ip
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: url
customDetails:
  UID: uid
  TimeGenerated: TimeGenerated
  BreachDate: breach_date
  FirstSeen: first_seen
  LastSeen: last_seen
  Impact: impact
  Recommendations: recommendations
  Source: source
  Description: description
  PassHash: pass_hash
alertDetailsOverride:
  alertDisplayNameFormat: "Employee Compromised - {{user_name}} - {{email}}"
  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

CyfirmaCompromisedAccounts_CL

Stage 2: where

| where TimeGenerated between (ago(timeFrame) .. now())
    and Category has "Compromised Employees"

Stage 3: extend

| extend 
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 4: summarize

| summarize arg_max(TimeGenerated, 
    url,
    ip,
    email,
    user_name,
    computer_name,
    operating_system,
    breach_date,
    first_seen,
    last_seen,
    impact,
    recommendations,
    description,
    source,
    pass_hash,
    ProductName,
    ProviderName
) by uid

Stage 5: sort

| sort by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categorymatch
  • Compromised Employees transforms: term
field:"Category" kind:match value:"Compromised Employees"

Output Fields

These fields are emitted when the rule matches.

FieldSource
uidsummarize

Detect entra token request via specific BOF (IOC based)

#
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This might be one of the silliest detections I have created. But since there is a Beacon Object File out there which can be used to directly request Entra ID access tokens from an active beacon on a device using a specific User Agent, we can easily detect this beacon file by flagging the funny user agent and / or scope identifier that is used.

MITRE ATT&CK Coverage

TacticTechniques
Execution
Credential Access

References

Telemetry Coverage

Rule Body

AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(5m)
| where UserAgent contains "ur mum" 
| where ResourceIdentity == "797f4846-ba00-4fd7-ba43-dac1f8f63013"

Stages and Predicates

Stage 1: source

AADNonInteractiveUserSignInLogs

Stage 2: where

| where TimeGenerated > ago(5m)

Stage 3: where

| where UserAgent contains "ur mum"

Stage 4: where

| where ResourceIdentity == "797f4846-ba00-4fd7-ba43-dac1f8f63013"

Indicators

These rows show field, operator, and value matches.

Detect Local File Inclusion(LFI) in web requests (ASIM Web Session)

#
Status
available
Severity
high
Time window
5m
Group by
Decoded_url, SrcHostname, SrcIpAddr, SrcUsername, Url
Source
github.com/Azure/Azure-Sentinel

'LFI vulnerabilities allow an attacker to read (and sometimes execute) files on the victim machine. This can be very dangerous because if the web server is misconfigured and running with high privileges, the attacker may gain access to sensitive information'

MITRE ATT&CK Coverage

Rule Body

id: 7bb55d05-ef39-4a40-8079-0bc3c05e7881
name: Detect Local File Inclusion(LFI) in web requests (ASIM Web Session)
description: |
  'LFI vulnerabilities allow an attacker to read (and sometimes execute) files on the victim machine. This can be very dangerous because if the web server is misconfigured and running with high privileges, the attacker may gain access to sensitive information'
severity: High
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
relevantTechniques:
  - T1190
  - T1133
  - T1059
query: |
  let lookback = 5m;
  let LFI_Indicators = materialize(externaldata(Indicators: string)
      [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/LocalFileInclusionIndicators.csv"] 
      with(format="csv", ignoreFirstRecord=True));
  let CustomLocalFileInclusionIndicators = (_ASIM_GetWatchlistRaw("Web_LocalFileInclusionIndicators") // Create new Watchlist and add your custom indicators(Optional)
      | extend
          Indicators = tostring(WatchlistItem["Indicators"])
      | project Indicators
      | where isnotempty(Indicators));
  let CombinedLFIList = union LFI_Indicators, CustomLocalFileInclusionIndicators;
  let knownLFIIndicators=toscalar(CombinedLFIList
      | where isnotempty(Indicators)
      | summarize make_set(Indicators, 1000));
  _Im_WebSession(starttime=ago(lookback), url_has_any=knownLFIIndicators, eventresult='Success')
  | where isnotempty(Url)
  | project Url, SrcIpAddr, SrcUsername, SrcHostname, TimeGenerated
  | extend Decoded_url = url_decode(Url)
  | where Decoded_url has_any (knownLFIIndicators)
  | summarize
      EventCount=count(),
      EventStartTime=min(TimeGenerated),
      EventEndTime=max(TimeGenerated)
      by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url
  | extend
      Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
      UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
  | order by EventCount desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventCount: EventCount
  Decoded_url: Decoded_url
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
alertDetailsOverride:
  alertDisplayNameFormat: "Potential Local File Inlcusion(LFI) performed by user '{{SrcUsername}}' from IP '{{SrcIpAddr}}'"
  alertDescriptionFormat: "User requested for URL '{{Url}}' which contains LFI related keywords or indicators. It suggests an attempt to traverse directories and access files outside the intended directory structure"
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 5m;
let CombinedLFIList = union LFI_Indicators, CustomLocalFileInclusionIndicators;

Let Binding: LFI_Indicators

let LFI_Indicators = materialize(externaldata(Indicators: string)
    [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/LocalFileInclusionIndicators.csv"] 
    with(format="csv", ignoreFirstRecord=True));

Let Binding: CustomLocalFileInclusionIndicators

let CustomLocalFileInclusionIndicators = (_ASIM_GetWatchlistRaw("Web_LocalFileInclusionIndicators")
    | extend
        Indicators = tostring(WatchlistItem["Indicators"])
    | project Indicators
    | where isnotempty(Indicators));

Let Binding: knownLFIIndicators used in Stages 1, 5

let knownLFIIndicators = toscalar(CombinedLFIList
    | where isnotempty(Indicators)
    | summarize make_set(Indicators, 1000));

Stage 1: source

_Im_WebSession(starttime=ago(lookback), url_has_any=knownLFIIndicators, eventresult='Success')

Stage 2: where

| where isnotempty(Url)

Stage 3: project

| project Url, SrcIpAddr, SrcUsername, SrcHostname, TimeGenerated

Stage 4: extend

| extend Decoded_url = url_decode(Url)

Stage 5: where

| where Decoded_url has_any (knownLFIIndicators)

Stage 6: summarize

| summarize
    EventCount=count(),
    EventStartTime=min(TimeGenerated),
    EventEndTime=max(TimeGenerated)
    by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url

Stage 7: extend

| extend
    Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
    UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")

Stage 8: sort

| order by EventCount desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Decoded_urlmatch
  • knownLFIIndicators transforms: term
field:"Decoded_url" kind:match value:"knownLFIIndicators"
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Decoded_urlsummarize
EventCountsummarize
EventEndTimesummarize
EventStartTimesummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
Urlsummarize
Nameextend
UPNSuffixextend

Detect LolDriver drop or load from unknown or unsigned process

#
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

Adversaries may use LolDrivers to elevate their privileges on a system. Regularly, their drop their own LolDrivers from their beacon process when the LolDriver is not yet present on the system. This is a detection use case to detect an unknown process dropping these LolDrivers.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

References

Telemetry Coverage

Rule Body

let LOLDrivers = externaldata(Category:string, KnownVulnerableSamples:dynamic, Verified:string ) [h@"https://www.loldrivers.io/api/drivers.json"]
     with (
       format=multijson,
       ingestionMapping=@'[{"Column":"Category","Properties":{"Path":"$.Category"}},{"Column":"KnownVulnerableSamples","Properties":{"Path":"$.KnownVulnerableSamples"}},{"Column":"Verified","Properties":{"Path":"$.Verified"}}]'
     )
    | mv-expand KnownVulnerableSamples
    | extend SHA1 = tostring(KnownVulnerableSamples.SHA1), SHA256 = tostring(KnownVulnerableSamples.SHA256)
;
let SHA1List = toscalar(
    LOLDrivers
    | summarize make_set(SHA1)
);
let SHA256List = toscalar(
    LOLDrivers
    | summarize make_set(SHA256)
);
let device_events = (
    DeviceEvents
    | where Timestamp > ago(1h)
    | where ActionType == "DriverLoaded"
    | where SHA1 in ( SHA1List ) or SHA256 in ( SHA256List )
);
let device_file_events = (
    DeviceFileEvents
    | where Timestamp > ago(1h)
    | where ActionType == "FileCreated"
    | where SHA1 in ( SHA1List ) or SHA256 in ( SHA256List )
);
union device_events, device_file_events
| invoke FileProfile(InitiatingProcessSHA1)
| where GlobalPrevalence < 1000 or SignatureState =~ "Unsigned"

Stages and Predicates

Let Binding: LOLDrivers

let LOLDrivers = externaldata(Category:string, KnownVulnerableSamples:dynamic, Verified:string ) [h@"https://www.loldrivers.io/api/drivers.json"]
     with (
       format=multijson,
       ingestionMapping=@'[{"Column":"Category","Properties":{"Path":"$.Category"}},{"Column":"KnownVulnerableSamples","Properties":{"Path":"$.KnownVulnerableSamples"}},{"Column":"Verified","Properties":{"Path":"$.Verified"}}]'
     )
    | mv-expand KnownVulnerableSamples
    | extend SHA1 = tostring(KnownVulnerableSamples.SHA1), SHA256 = tostring(KnownVulnerableSamples.SHA256);

Let Binding: SHA1List

let SHA1List = toscalar(
    LOLDrivers
    | summarize make_set(SHA1)
);

Let Binding: SHA256List

let SHA256List = toscalar(
    LOLDrivers
    | summarize make_set(SHA256)
);

Union (2 Sources)

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

Leg 1: device_events

Leg 2: device_file_events

Applied to the Combined Result

| invoke FileProfile(InitiatingProcessSHA1) | where GlobalPrevalence < 1000 or SignatureState =~ "Unsigned"

Indicators

These rows show field, operator, and value matches.

DLL Hijacking: Loading from an Unusual Directory

#
Group by
FileName, Process, SHA1
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different folder and making an application load it instead of the original DLL.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://posts.bluraven.io/detecting-dll-hijacking-attacks-part-1-bdb354685164
// Description: This query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different
//              folder and making an application load it instead of the original DLL.
//
// Query parameters:
let WinDevices = materialize (
    DeviceInfo
    | where Timestamp > ago(8d)
    | where OSPlatform startswith_cs "Windows"
    | summarize make_set(DeviceId)
    )
    ;
// Get Filenames that have more than 1 SHA1 and loaded by the same process
let FileNames = materialize (
    DeviceImageLoadEvents
    | where Timestamp > ago(8d)
    | where DeviceId in (WinDevices)
    | where isnotempty(SHA1) and isnotempty(InitiatingProcessFileName)
    | project FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
    | where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
    | where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
    | summarize hint.strategy=shuffle dcount(SHA1) by FileName, Process
    | where dcount_SHA1 > 1
    )
    ;
// From the Filenames, get SHA1 values and filter Filename-SHA1 if its loaded by a few proecsses and on a few devices
// Also, get first and last time of load of the file(based on SHA1, filename)
let Files = materialize (
    FileNames
    | join hint.strategy=shuffle kind=rightsemi (
        DeviceImageLoadEvents
        | where Timestamp > ago(8d)
        | where DeviceId in (WinDevices)
        | project Timestamp, DeviceName, FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
        | where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
        | where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
        | summarize hint.strategy=shuffle dcount(DeviceName), Process = make_set(Process), FirstLoad = min(Timestamp), LastLoad = max(Timestamp), count() by FileName, SHA1
        | where dcount_DeviceName < 3 and array_length(Process) < 3
        | mv-expand Process to typeof(string)
        )
        on FileName, Process
    )
    ;
// Files: Potentially suspicious files seen in the last 8d
// Next step: suspicious file (based on SHA1) should have been loaded from a location different than the other files(SHA1s) based on the same filename
// The suspicious file should have also been loaded recently
// Get files loaded in the last day (from the potentially suspicious files)
let Hashes = materialize (
    Files
    | where FirstLoad > ago(1d)
    | join (
        DeviceFileCertificateInfo
        | where Timestamp > ago(30d)
        | where not(IsTrusted )
        | summarize arg_max(Timestamp,*) by SHA1
        | project-away Timestamp, DeviceId, DeviceName) on SHA1
    | project-away SHA11
    )
    ;
// Get all image loads of the files that have the same name with the files in Hashes table(Hashes table only has the suspicious hash with its name from the last day)
Hashes
| join hint.strategy=shuffle kind=inner (
    DeviceImageLoadEvents
    | where Timestamp > ago(8d)
    | where DeviceId in (WinDevices)
    | project Timestamp, DeviceName, SHA1, FolderPath, FileName = tolower(FileName), Process = tolower(InitiatingProcessFileName), DLLDirectory = strcat(tolower(tostring(parse_path(FolderPath).DirectoryPath)), '\\'), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
    | where DLLDirectory  in ("c:\\windows\\", "c:\\windows\\system32\\", "c:\\windows\\syswow64\\", "c:\\windows\\winsxs\\") or (not(DLLDirectory startswith "c:\\windows\\"))
    | where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
    | extend NormalizedDLLDirectory = replace(@'(c|d):\\users\\.*?\\', @'c:\\users\\userxx\\',DLLDirectory)
    | extend NormalizedDLLDirectory = replace(@'\{.*\}', @'\{xxxxxxxxxx\}',NormalizedDLLDirectory) //{fe07d7-d438-4dd9-bb0f-5721658f4f}
    | extend NormalizedDLLDirectory = replace(@'\\[A-Za-z0-9-]+-[A-Za-z0-9]+\\', @'\\xxxxxxxxxx\\',NormalizedDLLDirectory ) //\fe07d7-d438-4dd9-bb0f-5721658f4f\
    | extend NormalizedDLLDirectory = replace(@'\d+\.\d+\.\d+\.\d+', @'X.Y.Z.T',NormalizedDLLDirectory) // ex: Edge\Application\104.0.1293.47\process.exe
    | extend NormalizedDLLDirectory = replace(@'-\d+\.\d+\.\d+', @'-X.Y.Z',NormalizedDLLDirectory)
    | extend NormalizedDLLDirectory = replace(@'c:\\windows\\assembly\\nativeimages_v\d\.\d\.\d+_\d{2}\\.*', @'c:\\windows\\assembly\\nativeimages_vX.Y.Z_T\\oneoffewsubfolders\\', NormalizedDLLDirectory)
    | extend NormalizedDLLDirectory = replace(@'c:\\programdata\\.*?\\microsoft\\teams\\',@'c:\\programdata\\userxxx\\microsoft\\teams\\',NormalizedDLLDirectory)
    | summarize hint.strategy=shuffle by FileName, SHA1, NormalizedDLLDirectory
    )
    on FileName
    | project-rename OtherSHA1 = SHA11
    // Flag suspicious hash
    | extend Suspicious = iff(SHA1==OtherSHA1, 'TRUE', 'FALSE')
    // group properties of suspicious and previous files separately
    // we are looking for a filename that was loaded from previously unknown location
    | summarize hint.strategy=shuffle PreviousDirs = make_set_if(NormalizedDLLDirectory, Suspicious == 'FALSE'),
                NewDir = make_set_if(NormalizedDLLDirectory, Suspicious == 'TRUE'),
                PreviousSHA1s = make_set_if(OtherSHA1, Suspicious == 'FALSE'),
                NewSHA1 = make_set_if(SHA1, Suspicious == 'TRUE')
                by  FileName
    // compare the directory of the suspicous file with the previous directories
    | extend diff = set_difference(NewDir, PreviousDirs)
    // filter if the new(suspicious) file is loaded from previously known directory
    | where diff != '[]'
    | order by FileName
    // if you get lots of false positives, uncomment the below section.
    // this section compares the directory names in an alternative way.
    // | mv-expand NewDir to typeof(string), PreviousDirs to typeof(string)
    // | extend prev = split(PreviousDirs, '\\'), new = split(NewDir, '\\')
    // | extend diff_new = set_difference(new, prev)
    // | extend diff_count = array_length(diff_new)
    // | where diff_count > 1
    // | project-away prev, new, diff_count, diff
    // get file profile info and filter based on global prevalence
    | mv-expand NewSHA1 to typeof(string)
    | invoke FileProfile(NewSHA1, 1000)
    | where GlobalPrevalence < 200 or isempty(GlobalPrevalence)

Stages and Predicates

let FileNames, let Files and let Hashes are inlined into the numbered stages below.

Let Binding: WinDevices used in Stages 1, 7

let WinDevices = materialize (
    DeviceInfo
    | where Timestamp > ago(8d)
    | where OSPlatform startswith_cs "Windows"
    | summarize make_set(DeviceId)
    );

Stage 1: source

let WinDevices

Stage 2: source

let FileNames

Stage 3: source

let Files

Stage 4: source

let Hashes

Stage 5: source

DeviceImageLoadEvents

Stage 6: where

where Timestamp > ago(691200s)

Stage 7: where

where DeviceId == "WinDevices"

Stage 8: where

where isnotempty(InitiatingProcessFileName) and isnotempty(SHA1)

Stage 9: project

project DLLDir, FileName, Process, ProcessDir, SHA1

Stage 10: where

where (not (DLLDir startswith @"c:\windows") or DLLDir in (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs"))

Stage 11: where

where ((ProcessDir has "AppData" and ProcessDir has "Users") or ProcessDir in (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs") or ProcessDir has "Program Files")

Stage 12: summarize

summarize by FileName, Process

Stage 13: where

where dcount_SHA1 > 1

Stage 14: join

join kind=rightsemi (DeviceImageLoadEvents) on FileName, Process

Stage 15: where

where FirstLoad > ago(86400s)

Stage 16: join

join (DeviceFileCertificateInfo) on SHA1

Stage 17: project-away

project-away SHA11

Stage 18: join

join kind=inner (DeviceImageLoadEvents) on FileName

Stage 19: project-rename

project-rename

Stage 20: extend

extend Suspicious
Suspicious =
ifSHA1 =~ OtherSHA1'TRUE'
else'FALSE'

Stage 21: summarize

summarize NewDir, NewSHA1, PreviousDirs, PreviousSHA1s by FileName

Stage 22: extend

extend diff

Stage 23: where

where diff != "[]"

Stage 24: sort

sort by FileName

Stage 25: mv-expand

mv-expand NewSHA1

Stage 26: invoke

invoke

Stage 27: where

where (isempty(GlobalPrevalence) or GlobalPrevalence < 200)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DLLDirin
  • c:\windows
  • c:\windows\system32
  • c:\windows\syswow64
  • c:\windows\winsxs
field:"DLLDir" kind:in
DLLDirectoryin
  • c:\windows\
  • c:\windows\system32\
  • c:\windows\syswow64\
  • c:\windows\winsxs\
field:"DLLDirectory" kind:in
DeviceIdin
  • WinDevices
field:"DeviceId" kind:in value:"WinDevices"
GlobalPrevalenceis_null
  • (no value, null check)
field:"GlobalPrevalence" kind:is_null
GlobalPrevalencelt
  • 200 transforms: number
field:"GlobalPrevalence" kind:lt value:"200"
InitiatingProcessFileNameis_not_null
  • (no value, null check)
field:"InitiatingProcessFileName" kind:is_not_null
ProcessDirin
  • c:\windows
  • c:\windows\system32
  • c:\windows\syswow64
  • c:\windows\winsxs
field:"ProcessDir" kind:in
ProcessDirmatch
  • AppData transforms: term
  • Program Files transforms: term
  • Users transforms: term
field:"ProcessDir" kind:match
SHA1is_not_null
  • (no value, null check)
field:"SHA1" kind:is_not_null
dcount_DeviceNamelt
  • 3 transforms: number
field:"dcount_DeviceName" kind:lt value:"3"
dcount_SHA1gt
  • 1 transforms: number
field:"dcount_SHA1" kind:gt value:"1"
diffne
  • []
field:"diff" kind:ne value:"[]"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IsTrustedeqtrueexcludes:IsTrusted field:"IsTrusted" value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
FileNamesummarize
NewDirsummarize
NewSHA1summarize
PreviousDirssummarize
PreviousSHA1ssummarize
diffextend

Fake computer account created

#
Severity
medium
Time window
1h
Group by
Computer, SubjectAccount, SubjectDomainName, SubjectLogonId, SubjectUserName, SubjectUserSid, TargetAccount, TargetDomainName, TargetSid, TargetUserName, UserPrincipalName
Author
Vasileios Paschalidis
Source
github.com/Azure/Azure-Sentinel

This query detects domain user accounts creation (event ID 4720) where the username ends with $. Accounts that end with $ are normally domain computer accounts and when they are created the event ID 4741 is generated instead.

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Telemetry Coverage

Rule Body

id: c1faf5e8-6958-11ec-90d6-0242ac120003
name: Fake computer account created
description: |
   'This query detects domain user accounts creation (event ID 4720) where the username ends with $. 
   Accounts that end with $ are normally domain computer accounts and when they are created the event ID 4741 is generated instead.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1564
query: |
  SecurityEvent
  | where EventID == 4720 and TargetUserName endswith "$"
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectUserName, SubjectDomainName, SubjectAccount, SubjectUserSid, SubjectLogonId, 
  TargetUserName, TargetDomainName, TargetAccount, TargetSid, UserPrincipalName
  | 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: SubjectAccount
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: SubjectUserSid
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetAccount
      - identifier: Name
        columnName: TargetUserName
      - identifier: NTDomain
        columnName: TargetDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: TargetSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Vasileios Paschalidis
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 4720 and TargetUserName endswith "$"

Stage 3: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectUserName, SubjectDomainName, SubjectAccount, SubjectUserSid, SubjectLogonId, 
TargetUserName, TargetDomainName, TargetAccount, TargetSid, UserPrincipalName

Stage 4: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 5: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =
ifDomainIndex != -1substring(Computer, (DomainIndex + 1))
elseComputer

Stage 6: project-away

| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4720 corpus 3 (kusto 3)
field:"EventID" kind:eq value:"4720"
TargetUserNameends_with
  • $ corpus 11 (sigma 7, splunk 2, kusto 2)
field:"TargetUserName" kind:ends_with value:"$"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
EndTimesummarize
StartTimesummarize
SubjectAccountsummarize
SubjectDomainNamesummarize
SubjectLogonIdsummarize
SubjectUserNamesummarize
SubjectUserSidsummarize
TargetAccountsummarize
TargetDomainNamesummarize
TargetSidsummarize
TargetUserNamesummarize
UserPrincipalNamesummarize
HostNameextend
HostNameDomainextend

Lookout - Critical Smishing and Phishing Alerts (v2)

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

'Detects critical smishing (SMS phishing) and phishing alerts from Lookout Mobile Risk API v2. This rule identifies sophisticated social engineering attacks including CEO fraud, credential harvesting, and malicious link campaigns targeting mobile devices. Leverages enhanced v2 smishing detection capabilities for comprehensive mobile threat protection.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access
Discovery
Collection

Rule Body

id: 7a3e5f9b-4c8d-4a2e-9f1b-6d8e2a4c7f9e
name: Lookout - Critical Smishing and Phishing Alerts (v2)
description: |
  'Detects critical smishing (SMS phishing) and phishing alerts from Lookout Mobile Risk API v2. This rule identifies sophisticated social engineering attacks including CEO fraud, credential harvesting, and malicious link campaigns targeting mobile devices. Leverages enhanced v2 smishing detection capabilities for comprehensive mobile threat protection.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: LookoutAPI
    dataTypes:
      - LookoutEvents
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
  - Collection
  - Discovery
relevantTechniques:
  - T1660
  - T1417
  - T1423
query: |
  LookoutEvents
  | where EventType == "SMISHING_ALERT"
  | where SmishingAlertSeverity in ("CRITICAL", "HIGH")
  | where SmishingAlertType in ("PHISHING_DETECTION", "FRAUD_DETECTION", "CREDENTIAL_HARVESTING")
  | extend 
      AlertRiskScore = case(
          SmishingAlertSeverity == "CRITICAL", 10,
          SmishingAlertSeverity == "HIGH", 8,
          SmishingAlertSeverity == "MEDIUM", 5,
          SmishingAlertSeverity == "LOW", 2,
          1
      ),
      ThreatCategory = case(
          SmishingAlertType == "PHISHING_DETECTION", "Phishing",
          SmishingAlertType == "FRAUD_DETECTION", "Fraud",
          SmishingAlertType == "CREDENTIAL_HARVESTING", "Credential Theft",
          SmishingAlertType == "MALICIOUS_LINK", "Malicious Link",
          "Other"
      ),
      ImpersonationRisk = case(
          SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive", "Executive Impersonation",
          SmishingAlertDescription has "IT" or SmishingAlertDescription has "support", "IT Support Impersonation", 
          SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial", "Financial Impersonation",
          SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package", "Delivery Impersonation",
          "Generic Phishing"
      )
  | extend DeviceRiskLevel = case(
      DeviceSecurityStatus == "THREATS_HIGH", "High",
      DeviceSecurityStatus == "THREATS_MEDIUM", "Medium", 
      DeviceSecurityStatus == "THREATS_LOW", "Low",
      "Unknown"
  )
  | extend CampaignIndicators = case(
      AlertRiskScore >= 8 and DeviceRiskLevel == "High", "Targeted Campaign",
      AlertRiskScore >= 6 and ImpersonationRisk != "Generic Phishing", "Sophisticated Attack",
      AlertRiskScore >= 5, "Coordinated Threat",
      "Isolated Incident"
  )
  | project
      TimeGenerated,
      EventId,
      SmishingAlertId,
      SmishingAlertType,
      SmishingAlertSeverity,
      SmishingAlertDescription,
      AlertRiskScore,
      ThreatCategory,
      ImpersonationRisk,
      CampaignIndicators,
      DeviceGuid,
      DevicePlatform,
      DeviceOSVersion,
      DeviceManufacturer,
      DeviceModel,
      DeviceEmailAddress,
      DeviceSecurityStatus,
      DeviceRiskLevel,
      TargetEmailAddress,
      TargetPlatform,
      ActorType,
      ActorGuid,
      ChangeType
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeviceEmailAddress
      - identifier: Name
        columnName: TargetEmailAddress
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceGuid
      - identifier: OSFamily
        columnName: DevicePlatform
      - identifier: OSVersion
        columnName: DeviceOSVersion
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: SmishingAlertDescription
customDetails:
  SmishAlertType: SmishingAlertType
  SmishSeverity: SmishingAlertSeverity
  AlertRiskScore: AlertRiskScore
  ThreatCategory: ThreatCategory
  ImpersonationRisk: ImpersonationRisk
  CampaignIndicators: CampaignIndicators
  DevicePlatform: DevicePlatform
  DeviceSecStatus: DeviceSecurityStatus
  DeviceRiskLevel: DeviceRiskLevel
alertDetailsOverride:
  alertDisplayNameFormat: "Critical Smishing Alert: {{ThreatCategory}} targeting {{DevicePlatform}} Device"
  alertDescriptionFormat: "{{SmishingAlertSeverity}} {{ThreatCategory}} attack on {{DevicePlatform}}"
  alertTacticsColumnName: ThreatCategory
  alertSeverityColumnName: SmishingAlertSeverity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
      - Host
    groupByAlertDetails:
      - SmishAlertType
      - DeviceGuid
    groupByCustomDetails:
      - ThreatCategory
      - ImpersonationRisk
      - CampaignIndicators
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 2.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

LookoutEvents

Stage 2: where

| where EventType == "SMISHING_ALERT"

Stage 3: where

| where SmishingAlertSeverity in ("CRITICAL", "HIGH")

Stage 4: where

| where SmishingAlertType in ("PHISHING_DETECTION", "FRAUD_DETECTION", "CREDENTIAL_HARVESTING")

Stage 5: extend (3 consecutive steps)

| extend 
    AlertRiskScore = case(
        SmishingAlertSeverity == "CRITICAL", 10,
        SmishingAlertSeverity == "HIGH", 8,
        SmishingAlertSeverity == "MEDIUM", 5,
        SmishingAlertSeverity == "LOW", 2,
        1
    ),
    ThreatCategory = case(
        SmishingAlertType == "PHISHING_DETECTION", "Phishing",
        SmishingAlertType == "FRAUD_DETECTION", "Fraud",
        SmishingAlertType == "CREDENTIAL_HARVESTING", "Credential Theft",
        SmishingAlertType == "MALICIOUS_LINK", "Malicious Link",
        "Other"
    ),
    ImpersonationRisk = case(
        SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive", "Executive Impersonation",
        SmishingAlertDescription has "IT" or SmishingAlertDescription has "support", "IT Support Impersonation", 
        SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial", "Financial Impersonation",
        SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package", "Delivery Impersonation",
        "Generic Phishing"
    )
| extend DeviceRiskLevel = case(
    DeviceSecurityStatus == "THREATS_HIGH", "High",
    DeviceSecurityStatus == "THREATS_MEDIUM", "Medium", 
    DeviceSecurityStatus == "THREATS_LOW", "Low",
    "Unknown"
)
| extend CampaignIndicators = case(
    AlertRiskScore >= 8 and DeviceRiskLevel == "High", "Targeted Campaign",
    AlertRiskScore >= 6 and ImpersonationRisk != "Generic Phishing", "Sophisticated Attack",
    AlertRiskScore >= 5, "Coordinated Threat",
    "Isolated Incident"
)
AlertRiskScore =
ifSmishingAlertSeverity == "CRITICAL"10
elifSmishingAlertSeverity == "HIGH"8
elifSmishingAlertSeverity == "MEDIUM"5
elifSmishingAlertSeverity == "LOW"2
else1
ImpersonationRisk =
if(SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive")"Executive Impersonation"
elif(SmishingAlertDescription has "IT" or SmishingAlertDescription has "support")"IT Support Impersonation"
elif(SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial")"Financial Impersonation"
elif(SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package")"Delivery Impersonation"
else"Generic Phishing"
ThreatCategory =
ifSmishingAlertType == "PHISHING_DETECTION""Phishing"
elifSmishingAlertType == "FRAUD_DETECTION""Fraud"
elifSmishingAlertType == "CREDENTIAL_HARVESTING""Credential Theft"
elifSmishingAlertType == "MALICIOUS_LINK""Malicious Link"
else"Other"

Stage 6: project

| project
    TimeGenerated,
    EventId,
    SmishingAlertId,
    SmishingAlertType,
    SmishingAlertSeverity,
    SmishingAlertDescription,
    AlertRiskScore,
    ThreatCategory,
    ImpersonationRisk,
    CampaignIndicators,
    DeviceGuid,
    DevicePlatform,
    DeviceOSVersion,
    DeviceManufacturer,
    DeviceModel,
    DeviceEmailAddress,
    DeviceSecurityStatus,
    DeviceRiskLevel,
    TargetEmailAddress,
    TargetPlatform,
    ActorType,
    ActorGuid,
    ChangeType

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorGuidproject
ActorTypeproject
AlertRiskScoreproject
CampaignIndicatorsproject
ChangeTypeproject
DeviceEmailAddressproject
DeviceGuidproject
DeviceManufacturerproject
DeviceModelproject
DeviceOSVersionproject
DevicePlatformproject
DeviceRiskLevelproject
DeviceSecurityStatusproject
EventIdproject
ImpersonationRiskproject
SmishingAlertDescriptionproject
SmishingAlertIdproject
SmishingAlertSeverityproject
SmishingAlertTypeproject
TargetEmailAddressproject
TargetPlatformproject
ThreatCategoryproject
TimeGeneratedproject

Non Domain Controller Active Directory Replication

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

This query detects potential attempts by non-computer accounts (non domain controllers) to retrieve/synchronize an active directory object leveraging directory replication services (DRS). A Domain Controller (computer account) would usually be performing these actions in a domain environment. Another detection rule can be created to cover domain controllers accounts doing at rare times. A domain user with privileged permissions to use directory replication services is rare.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: b9d2eebc-5dcb-4888-8165-900db44443ab
name: Non Domain Controller Active Directory Replication
description: |
  'This query detects potential attempts by non-computer accounts (non domain controllers) to retrieve/synchronize an active directory object leveraging directory replication services (DRS).
  A Domain Controller (computer account) would usually be performing these actions in a domain environment. Another detection rule can be created to cover domain controllers accounts doing at rare times.
  A domain user with privileged permissions to use directory replication services is rare.'
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1003.006
query: |
  // Enter a reference list of hostnames for your DC servers
  //let DCServersList = dynamic (["DC01.simulandlabs.com","DC02.simulandlabs.com"]);
  SecurityEvent
  //| where Computer in (DCServersList)
  | where EventID == 4662 and ObjectServer == 'DS'
  | where AccountType != 'Machine'
  | where Properties has '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' //DS-Replication-Get-Changes
      or Properties has '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' //DS-Replication-Get-Changes-All
      or Properties has '89e95b76-444d-4c62-991a-0facbeda640c' //DS-Replication-Get-Changes-In-Filtered-Set
  | project TimeGenerated, Account, Activity, Properties, SubjectLogonId, Computer
  | join kind=leftouter
  (
      SecurityEvent
      //| where Computer in (DCServersList)
      | where EventID == 4624 and LogonType == 3
      | where AccountType != 'Machine'
      | project TargetLogonId, IpAddress, Computer
  )
  on $left.SubjectLogonId == $right.TargetLogonId and $left.Computer == $right.Computer
  | project-reorder TimeGenerated, Computer, Account, IpAddress
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountNTDomain = tostring(split(Account, "\\")[0]), AccountName = tostring(split(Account, "\\")[1])

entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain 
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 4662 and ObjectServer == 'DS'

Stage 3: where

| where AccountType != 'Machine'

Stage 4: where

| where Properties has '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
    or Properties has '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2'
    or Properties has '89e95b76-444d-4c62-991a-0facbeda640c'

Stage 5: project

| project TimeGenerated, Account, Activity, Properties, SubjectLogonId, Computer

Stage 6: join

| join kind=leftouter
(
    SecurityEvent
    | where EventID == 4624 and LogonType == 3
    | where AccountType != 'Machine'
    | project TargetLogonId, IpAddress, Computer
)
on $left.SubjectLogonId == $right.TargetLogonId and $left.Computer == $right.Computer

Stage 7: project-reorder

project-reorder

Stage 8: extend (3 consecutive steps)

extend AccountNTDomain, AccountName, DomainIndex, HostName, HostNameDomain

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountTypene
  • Machine
field:"AccountType" kind:ne value:"Machine"
EventIDeq
  • 4624
  • 4662 corpus 6 (kusto 3, elastic 2, chronicle 1)
field:"EventID" kind:eq
LogonTypeeq
  • 3
field:"LogonType" kind:eq value:"3"
ObjectServereq
  • DS corpus 2 (kusto 2)
field:"ObjectServer" kind:eq value:"DS"
Propertiesmatch
  • 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 transforms: term
  • 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 transforms: term
  • 89e95b76-444d-4c62-991a-0facbeda640c transforms: term
field:"Properties" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
Activityproject
Computerproject
Propertiesproject
SubjectLogonIdproject
TimeGeneratedproject
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

OCI - Insecure metadata endpoint

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

'Detects insecure metadata endpoint.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 9c4b1b9c-6462-41ce-8f2e-ce8c104331fc
name: OCI - Insecure metadata endpoint
description: |
  'Detects insecure metadata endpoint.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: OracleCloudInfrastructureLogsConnector
    dataTypes:
      - OCILogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1069
query: |
  OCILogs
  | where data_request_headers_oci_original_url_s contains '/opc/v1' or data_request_headers_oci_original_url_s contains '/openstack'
  | where HttpStatusCode == 200
  | extend IPCustomEntity = DstIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

OCILogs

Stage 2: where

| where data_request_headers_oci_original_url_s contains '/opc/v1' or data_request_headers_oci_original_url_s contains '/openstack'

Stage 3: where

| where HttpStatusCode == 200

Stage 4: extend

| extend IPCustomEntity = DstIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Palo Alto Prisma Cloud - Multiple failed logins for user

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

'Detects multiple failed logins for the same user account.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 4f688252-bf9b-4136-87bf-d540b5be1050
name: Palo Alto Prisma Cloud - Multiple failed logins for user
description: |
  'Detects multiple failed logins for the same user account.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: PaloAltoPrismaCloud
    dataTypes:
      - PaloAltoPrismaCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let threshold = 10;
  PaloAltoPrismaCloud
  | where ResourceType =~ 'Login'
  | where EventResult =~ 'Failed'
  | where EventMessage !has 'access key'
  | summarize count() by UserName, bin(TimeGenerated, 5m)
  | where count_ > threshold
  | extend AccountCustomEntity = UserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

PaloAltoPrismaCloud

Stage 2: where

| where ResourceType =~ 'Login'

Stage 3: where

| where EventResult =~ 'Failed'

Stage 4: where

| where EventMessage !has 'access key'

Stage 5: summarize

| summarize count() by UserName, bin(TimeGenerated, 5m)

Stage 6: where

| where count_ > threshold

Stage 7: extend

| extend AccountCustomEntity = UserName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EventMessagematchaccess keyexcludes:EventMessage field:"EventMessage" value:"access key"

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserNamesummarize
AccountCustomEntityextend

Pathlock TDnR - SAP Download Observer Events

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

Detects download events captured by the Pathlock DownLoad Observer in SAP, forwarded to Microsoft Sentinel. Unusual download patterns may indicate bulk data exfiltration, unauthorized extraction of sensitive SAP data, or insider threat activity involving large-scale data downloads.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c51
name: Pathlock TDnR - SAP Download Observer Events
kind: Scheduled
description: >-
  Detects download events captured by the Pathlock DownLoad Observer in SAP, forwarded to
  Microsoft Sentinel. Unusual download patterns may indicate bulk data exfiltration, unauthorized
  extraction of sensitive SAP data, or insider threat activity involving large-scale data downloads.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SAST_DO"
  | 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 == "SAST_DO"

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

PROD (TM001.1) - GROUP - Added to Group Outside the Object Tier Level

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

Added an object to a member of a Tier Model group that is outside of the same Tier. Example a Tier 0 user added to a Tier 1 group.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rule Body

id: b300c7f8-dc5e-4fc6-879b-63f0013358ce
name: 'PROD (TM001.1) - GROUP - Added to Group Outside the Object Tier Level'
description: |
  Added an object to a member of a Tier Model group that is outside of the same Tier. Example a Tier 0 user added to a Tier 1 group.
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.002
query: |
  SecurityEvent 
  | where EventID == 5136
      and Activity has "modified"
      and (EventData matches regex @"(?i)OU=Tier 0 Accounts" 
           or EventData matches regex @"(?i)OU=Tier 1 Accounts" 
           or EventData matches regex @"(?i)OU=Tier 2 Accounts"
           or EventData matches regex @"(?i)OU=Tier 0 Service Accounts" 
           or EventData matches regex @"(?i)OU=Tier 1 Service Accounts" 
           or EventData matches regex @"(?i)OU=Tier 2 Service Accounts"
           or EventData matches regex @"(?i)OU=Tier 0 Groups"
           or EventData matches regex @"(?i)OU=Tier 1 Groups"
           or EventData matches regex @"(?i)OU=Tier 2 Groups"
           or EventData matches regex @"(?i)OU=Tier 0 PAW"
           or EventData matches regex @"(?i)OU=Tier 1 PAW"
           or EventData matches regex @"(?i)OU=Tier 2 PAW")
      and EventData has '"AttributeLDAPDisplayName">member'
      and EventData has 'OperationType">%%14674'
  | extend GroupName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
  | extend GroupName = extract("(?i)CN=([^,]+)", 1, GroupName_e)
  | where GroupName != "Protected Users"
  | where GroupName != "Allowed RODC Password Replication Group"
  | extend GroupTier_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
  | extend GroupTier = extract("(?i)OU=(Tier [^ ]+)", 1, GroupTier_e)
  | extend GroupTier = iff(isempty(GroupTier), "Outside Tier Model", GroupTier)
  | extend ObjectName_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
  | extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
  | extend ObjectTier_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
  | extend ObjectTier = extract("(?i)OU=(Tier [^ ]+)", 1, ObjectTier_e)
  | extend ObjectTier = iff(isempty(ObjectTier), "Outside Tier Model", ObjectTier)
  | extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
  | extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
  | extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))
  | where GroupTier != ObjectTier
  | project TimeGenerated 
          , ObjectName
          , ObjectTier
          , ObjectType = AccountType
          , GroupName
          , GroupTier
          , ActivityType
          , OperationType
          , Account
          , Domain
          , Computer
          , Channel
          , EventID
          , EventData
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ObjectName
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: GroupName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
customDetails:
  ObjectName: ObjectName
  ObjectTier: ObjectTier
  ObjectType: ObjectType
  GroupName: GroupName
  GroupTier: GroupTier
  ActivityType: ActivityType
  OperationType: OperationType
  Account: Account
  Domain: Domain
  Channel: Channel
  EventID: EventID
  EventData: EventData
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5M
    matchingMethod: Selected
    groupByEntities:
      - Account
    groupByCustomDetails:
      - ObjectName
alertDetailsOverride:
  alertDisplayNameFormat: '(TM001.1) A {{ObjectTier}} object was added to non-{{ObjectTier}} group {{GroupName}}'
  alertDescriptionFormat: |
    Tier Model Alert TM001 - A {{ObjectTier}} object was added to non-{{ObjectTier}} group {{GroupName}}.

    Resolution - Remove the object from the group
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 5136
    and Activity has "modified"
    and (EventData matches regex @"(?i)OU=Tier 0 Accounts" 
         or EventData matches regex @"(?i)OU=Tier 1 Accounts" 
         or EventData matches regex @"(?i)OU=Tier 2 Accounts"
         or EventData matches regex @"(?i)OU=Tier 0 Service Accounts" 
         or EventData matches regex @"(?i)OU=Tier 1 Service Accounts" 
         or EventData matches regex @"(?i)OU=Tier 2 Service Accounts"
         or EventData matches regex @"(?i)OU=Tier 0 Groups"
         or EventData matches regex @"(?i)OU=Tier 1 Groups"
         or EventData matches regex @"(?i)OU=Tier 2 Groups"
         or EventData matches regex @"(?i)OU=Tier 0 PAW"
         or EventData matches regex @"(?i)OU=Tier 1 PAW"
         or EventData matches regex @"(?i)OU=Tier 2 PAW")
    and EventData has '"AttributeLDAPDisplayName">member'
    and EventData has 'OperationType">%%14674'

Stage 3: extend

| extend GroupName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)

Stage 4: extend

| extend GroupName = extract("(?i)CN=([^,]+)", 1, GroupName_e)

Stage 5: where

| where GroupName != "Protected Users"

Stage 6: where

| where GroupName != "Allowed RODC Password Replication Group"

Stage 7: extend (11 consecutive steps)

| extend GroupTier_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend GroupTier = extract("(?i)OU=(Tier [^ ]+)", 1, GroupTier_e)
| extend GroupTier = iff(isempty(GroupTier), "Outside Tier Model", GroupTier)
| extend ObjectName_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
| extend ObjectTier_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectTier = extract("(?i)OU=(Tier [^ ]+)", 1, ObjectTier_e)
| extend ObjectTier = iff(isempty(ObjectTier), "Outside Tier Model", ObjectTier)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))

Stage 8: where

| where GroupTier != ObjectTier

Stage 9: project

| project TimeGenerated 
        , ObjectName
        , ObjectTier
        , ObjectType = AccountType
        , GroupName
        , GroupTier
        , ActivityType
        , OperationType
        , Account
        , Domain
        , Computer
        , Channel
        , EventID
        , EventData

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activitymatch
  • modified transforms: term corpus 10 (kusto 10)
field:"Activity" kind:match value:"modified"
EventDatamatch
  • "AttributeLDAPDisplayName">member transforms: term corpus 2 (kusto 2)
  • OperationType">%%14674 transforms: term corpus 3 (kusto 3)
field:"EventData" kind:match
EventDataregex_match
  • (?i)OU=Tier 0 Accounts corpus 4 (kusto 4)
  • (?i)OU=Tier 0 Groups corpus 3 (kusto 3)
  • (?i)OU=Tier 0 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 0 Service Accounts corpus 4 (kusto 4)
  • (?i)OU=Tier 1 Accounts corpus 4 (kusto 4)
  • (?i)OU=Tier 1 Groups corpus 3 (kusto 3)
  • (?i)OU=Tier 1 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 1 Service Accounts corpus 4 (kusto 4)
  • (?i)OU=Tier 2 Accounts corpus 4 (kusto 4)
  • (?i)OU=Tier 2 Groups corpus 3 (kusto 3)
  • (?i)OU=Tier 2 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 2 Service Accounts corpus 4 (kusto 4)
field:"EventData" kind:regex_match
EventIDeq
  • 5136 corpus 20 (kusto 18, elastic 2)
field:"EventID" kind:eq value:"5136"
GroupNamene
  • Allowed RODC Password Replication Group
  • Protected Users
field:"GroupName" kind:ne

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
GroupNameproject
GroupTierproject
ObjectNameproject
ObjectTierproject
ObjectTypeproject
OperationTypeproject
TimeGeneratedproject

Progress MOVEIt File transfer above threshold

#
Severity
medium
Time window
1d
Group by
Computer, ipaddress, userdisplayname
Source
github.com/Azure/Azure-Sentinel

Identifies Progress MOVEIt File Transfers above certain threshold in a 15min time period. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rule Body

id: 9bd18b63-f1ca-4375-95db-39fda00bfe20
name: Progress MOVEIt File transfer above threshold
description: |
  'Identifies Progress MOVEIt File Transfers above certain threshold in a 15min time period.
   Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1020
query: |
  let threshold = 500;
  Event
  | where EventID == 0 and Source == "MOVEit DMZ Audit" and EventLog == "Application"
  | where RenderedDescription has_any ("Downloaded", "Uploaded")
  | parse RenderedDescription with * "User '" userdisplayname "' (" *
  | parse RenderedDescription with * "IPAddress: " ipaddress " FileID:" * " FileName: " filename " FolderID: " folderid " FolderPath: " folderpath " Username: " username " AgentBrand: " *
  | summarize count_=count(), fileslist=make_set(filename) by Computer,userdisplayname,ipaddress,bin(TimeGenerated, 1d)
  | where count_ >= threshold
  | extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: userdisplayname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ipaddress
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileSample
customDetails:
  TransferCount: count_
  FilesList: fileslist
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: Selected
    groupByEntities:
    - Account
    - Host
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 500;

Stage 1: source

Event

Stage 2: where

| where EventID == 0 and Source == "MOVEit DMZ Audit" and EventLog == "Application"

Stage 3: where

| where RenderedDescription has_any ("Downloaded", "Uploaded")

Stage 4: parse

| parse RenderedDescription with * "User '" userdisplayname "' (" *

Stage 5: parse

| parse RenderedDescription with * "IPAddress: " ipaddress " FileID:" * " FileName: " filename " FolderID: " folderid " FolderPath: " folderpath " Username: " username " AgentBrand: " *

Stage 6: summarize

| summarize count_=count(), fileslist=make_set(filename) by Computer,userdisplayname,ipaddress,bin(TimeGenerated, 1d)

Stage 7: where

| where count_ >= threshold

Stage 8: extend

| extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
FileSample =
iffileslist == 1tostring(fileslist[0])
elsestrcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 0 corpus 2 (kusto 2)
field:"EventID" kind:eq value:"0"
EventLogeq
  • Application corpus 2 (kusto 2)
field:"EventLog" kind:eq value:"Application"
RenderedDescriptionmatch
  • Downloaded transforms: term corpus 2 (kusto 2)
  • Uploaded transforms: term corpus 2 (kusto 2)
field:"RenderedDescription" kind:match
count_ge
  • 500 transforms: number
field:"count_" kind:ge value:"500"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
count_summarize
fileslistsummarize
ipaddresssummarize
userdisplaynamesummarize
FileSampleextend

TI map Email entity to PaloAlto CommonSecurityLog

#
Severity
medium
Time window
14d
Group by
$left.EmailSenderAddress, $right.DestinationUserID, DestinationUserID, IndicatorId
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in CommonSecurityLog table from any Email IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: ffcd575b-3d54-482a-a6d8-d0de13b6ac63
name: TI map Email entity to PaloAlto CommonSecurityLog
description: |
  'Identifies a match in CommonSecurityLog table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  ThreatIntelligenceIndicator
  //Filtering the table for Email related IOCs
  | where isnotempty(EmailSenderAddress)
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (
      CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
      // Filtering PAN Logs for specific event type to match relevant email entities
      | where DeviceVendor == "Palo Alto Networks" and  DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
      | extend DestinationUserID = tolower(DestinationUserID)
      | where DestinationUserID matches regex emailregex
      | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  )
  on $left.EmailSenderAddress == $right.DestinationUserID
  | where CommonSecurityLog_TimeGenerated < ExpirationDateTime
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by IndicatorId, DestinationUserID
  | project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient,
  EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, DestinationUserID, DeviceEventClassID, LogSeverity, DeviceAction, SourceIP, SourcePort,
  DestinationIP, DestinationPort, Protocol, ApplicationProtocol
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: DestinationUserID
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.6
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(EmailSenderAddress)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 5: where

| where Active == true and ExpirationDateTime > now()

Stage 6: join

| join kind=innerunique (
    CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
    | where DeviceVendor == "Palo Alto Networks" and  DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
    | extend DestinationUserID = tolower(DestinationUserID)
    | where DestinationUserID matches regex emailregex
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.DestinationUserID

Stage 7: where

where CommonSecurityLog_TimeGenerated < ExpirationDateTime

Stage 8: summarize

summarize CommonSecurityLog_TimeGenerated by IndicatorId, DestinationUserID

Stage 9: project

project ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, ConfidenceScore, Description, DestinationIP, DestinationPort, DestinationUserID, DeviceAction, DeviceEventClassID, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, LogSeverity, Protocol, SourceIP, SourcePort, ThreatType, Url

Stage 10: 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
ConfidenceScoreproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DestinationUserIDproject
DeviceActionproject
DeviceEventClassIDproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
LogSeverityproject
Protocolproject
SourceIPproject
SourcePortproject
ThreatTypeproject
Urlproject
timestampextend

Back to all Kusto rules