Detection rules › Kusto

Kusto rules: 11

Abnormal Port to Protocol

#
Status
available
Severity
medium
Time window
8d
Group by
$left.AlertTimeDstPort, $left.AlertTimeProtocol, $right.LearningTimeDstPort, $right.LearningTimeProtocol, AlertTimeDstPort, AlertTimeProtocol, Fqdn, LearningTimeDstPort, LearningTimeProtocol, SourceIp
Source
github.com/Azure/Azure-Sentinel

'Identifies communication for well known protocol over a non-standard port based on learning period activity. This can indicate malicious communication (C2) or exfiltration by attackers trying to communicate over known ports (22:SSH, 80:HTTP) but dont use the known protocol headers to match the port number. Configurable Parameters: - Learning period time - learning period for protocol learning in days. Default is set to 7.'

MITRE ATT&CK Coverage

Rule Body

id: 826f930c-2f25-4508-8e75-a95b809a4e15
name: Abnormal Port to Protocol
description: |
  'Identifies communication for well known protocol over a non-standard port based on learning period activity. This can indicate malicious communication (C2) or exfiltration by attackers trying to communicate over known ports (22:SSH, 80:HTTP) but dont use the known protocol headers to match the port number.
  
  Configurable Parameters:
  
  - Learning period time - learning period for protocol learning in days. Default is set to 7.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureFirewall
    dataTypes: 
      - AzureDiagnostics
      - AZFWApplicationRule
      - AZFWNetworkRule
queryFrequency: 1h
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
  - DefenseEvasion
relevantTechniques:
  - T1041
  - T1571
  - T1572
query: |
  let LearningPeriod = 7d;
  let RunTime = 1h;
  let StartLearningPeriod = LearningPeriod + RunTime;
  let DetectionWindowStart = ago(RunTime);
  let LearningPortToProtocol1 =  (AzureDiagnostics
  | where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
  | where OperationName == "AzureFirewallApplicationRuleLog"
  | extend msg_s= column_ifexists('msg_s',Message)
  | parse msg_s with Protocol " request from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int "." *
  | where isnotempty(DestinationPort)
  | summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
  let LearningPortToProtocol2 = (AZFWNetworkRule
  | where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
  | where isnotempty(DestinationPort)
  | extend Fqdn = DestinationIp
  | summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
  let LearningPortToProtocol3 = (AZFWApplicationRule
  | where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
  | where isnotempty(DestinationPort)
  | summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);
  let AlertTimePortToProtocol1 = (AzureDiagnostics
  | where TimeGenerated between (DetectionWindowStart .. now())
  | where OperationName == "AzureFirewallApplicationRuleLog"
  | extend msg_s= column_ifexists('msg_s',Message)
  | parse msg_s with Protocol " request from " SourceIp ":" SourcePort " to " Fqdn ":" DestinationPort:int "." *
  | where isnotempty(DestinationPort)
  | summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
  let AlertTimePortToProtocol2 = (AZFWNetworkRule
  | where TimeGenerated between (DetectionWindowStart .. now())
  | where isnotempty(DestinationPort)
  | extend Fqdn = DestinationIp
  | summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
  let AlertTimePortToProtocol3 = (AZFWApplicationRule
  | where TimeGenerated between (DetectionWindowStart .. now())
  | where isnotempty(DestinationPort)
  | summarize AlertTimeCount = count() by AlertTimeDstPort = DestinationPort, AlertTimeProtocol = Protocol);
  (union isfuzzy=true 
  (AlertTimePortToProtocol1 
  | join kind=leftouter (LearningPortToProtocol1) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
  | where LearningTimeProtocol != AlertTimeProtocol),
  (AlertTimePortToProtocol2 
  | join kind=leftanti (LearningPortToProtocol2) on $left.AlertTimeDstPort == $right.LearningTimeDstPort, $left.AlertTimeProtocol == $right.LearningTimeProtocol),
  (AlertTimePortToProtocol3 
  | join kind=leftouter (LearningPortToProtocol3) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
  | where LearningTimeProtocol != AlertTimeProtocol))
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Fqdn
customDetails:
  AbnormalPort: AlertTimeDstPort
  NetworkProtocol: AlertTimeProtocol
alertDetailsOverride:
  alertDisplayNameFormat: 'Abnormal Port to Protocol Communication Detected from {{SourceIp}} to {{Fqdn}}'
  alertDescriptionFormat: 'Communication was observed over port {{AlertTimeDstPort}} using protocol {{AlertTimeProtocol}}, which is different from the protocol observed during the learning period. This could indicate potential malicious activity such as C2 communication or data exfiltration.'
version: 1.1.5
kind: Scheduled

Stages and Predicates

Parameters

let LearningPeriod = 7d;
let RunTime = 1h;
let StartLearningPeriod = LearningPeriod + RunTime;
let DetectionWindowStart = ago(RunTime);

Let Binding: LearningPortToProtocol1

let LearningPortToProtocol1 = (AzureDiagnostics
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where OperationName == "AzureFirewallApplicationRuleLog"
| extend msg_s= column_ifexists('msg_s',Message)
| parse msg_s with Protocol " request from " SourceIp ":" SourcePort:int " to " Fqdn ":" DestinationPort:int "." *
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);

Let Binding: LearningPortToProtocol2

let LearningPortToProtocol2 = (AZFWNetworkRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| extend Fqdn = DestinationIp
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);

Let Binding: LearningPortToProtocol3

let LearningPortToProtocol3 = (AZFWApplicationRule
| where TimeGenerated between (ago(StartLearningPeriod) .. ago(RunTime))
| where isnotempty(DestinationPort)
| summarize LearningTimeCount = count() by LearningTimeDstPort = DestinationPort, LearningTimeProtocol = Protocol, SourceIp, Fqdn);

Union isfuzzy=true (3 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: AlertTimePortToProtocol1, AlertTimePortToProtocol2, AlertTimePortToProtocol3

Leg 1: AlertTimePortToProtocol1

AlertTimePortToProtocol1 
| join kind=leftouter (LearningPortToProtocol1) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol

Leg 2: AlertTimePortToProtocol2

AlertTimePortToProtocol2 
| join kind=leftanti (LearningPortToProtocol2) on $left.AlertTimeDstPort == $right.LearningTimeDstPort, $left.AlertTimeProtocol == $right.LearningTimeProtocol

Leg 3: AlertTimePortToProtocol3

AlertTimePortToProtocol3 
| join kind=leftouter (LearningPortToProtocol3) on $left.AlertTimeDstPort == $right.LearningTimeDstPort
| where LearningTimeProtocol != AlertTimeProtocol

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationPortis_not_null(no value, null check)excludes:DestinationPort

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fqdnsummarize
LearningTimeDstPortsummarize
LearningTimeProtocolsummarize
SourceIpsummarize

Cisco Umbrella - Windows PowerShell User-Agent Detected

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

'Rule helps to detect Powershell user-agent activity by an unusual process other than a web browser.'

MITRE ATT&CK Coverage

TacticTechniques
StealthNo specific technique
Command & ControlNo specific technique

Rule Body

id: b12b3dab-d973-45af-b07e-e29bb34d8db9
name: Cisco Umbrella - Windows PowerShell User-Agent Detected
description: |
  'Rule helps to detect Powershell user-agent activity by an unusual process other than a web browser.'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes:
      - Cisco_Umbrella_proxy_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - DefenseEvasion
query: |
  let timeframe = 15m;
  Cisco_Umbrella
  | where EventType == "proxylogs"
  | where TimeGenerated > ago(timeframe)
  | where HttpUserAgentOriginal contains "WindowsPowerShell"
  | extend Message = "Windows PowerShell User Agent"
  | project Message, SrcIpAddr, DstIpAddr, UrlOriginal, TimeGenerated,HttpUserAgentOriginal
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlOriginal
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 15m;

Stage 1: source

Cisco_Umbrella

Stage 2: where

| where EventType == "proxylogs"

Stage 3: where

| where TimeGenerated > ago(timeframe)

Stage 4: where

| where HttpUserAgentOriginal contains "WindowsPowerShell"

Stage 5: extend

| extend Message = "Windows PowerShell User Agent"

Stage 6: project

| project Message, SrcIpAddr, DstIpAddr, UrlOriginal, TimeGenerated,HttpUserAgentOriginal

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstIpAddrproject
HttpUserAgentOriginalproject
Messageproject
SrcIpAddrproject
TimeGeneratedproject
UrlOriginalproject

Claroty - Threat detected

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

Detects Claroty events where EventOriginalType or EventType contains 'Threat', identifying Threat-related activity and surfacing the destination IP address for investigation.

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance
Discovery

Rule Body

id: 731e5ac4-7fe1-4b06-9941-532f2e008bb3
name: Claroty - Threat detected
description: Detects Claroty events where EventOriginalType or EventType contains 'Threat', identifying Threat-related activity
  and surfacing the destination IP address for investigation.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Claroty
    dataTypes:
      - ClarotyEvent
  - connectorId: ClarotyAma
    dataTypes:
      - ClarotyEvent
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - Reconnaissance
relevantTechniques:
  - T1018
  - T1595
query: |
  ClarotyEvent
  | where EventOriginalType has 'Threat' or EventType has 'Threat'
  | extend IPCustomEntity = DstIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
alertDetailsOverride:
  alertDisplayNameFormat: Claroty Threat detected on {{IPCustomEntity}}
  alertDescriptionFormat: Claroty event type {{EventType}} or original type {{EventOriginalType}} matched Threat on {{IPCustomEntity}}
customDetails:
  EventOriginalType: EventOriginalType
  EventType: EventType
  TimeGenerated: TimeGenerated
version: 1.0.5
kind: Scheduled

Stages and Predicates

Stage 1: source

ClarotyEvent

Stage 2: where

| where EventOriginalType has 'Threat' or EventType has 'Threat'

Stage 3: 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

Cloudflare - Client request from country in blocklist

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

'Detects requests from countries which are in blocklist.'

MITRE ATT&CK Coverage

Rule Body

id: 40554544-6e4a-4413-8d14-bf2de939c5d0
name: Cloudflare - Client request from country in blocklist
description: |
  'Detects requests from countries which are in blocklist.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let bl_countries = dynamic(['cn', 'hk']);
  Cloudflare
  | where SrcGeoCountry in~ (bl_countries)
  | extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: CompleteUrl
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let bl_countries = dynamic(['cn', 'hk']);

Stage 1: source

Cloudflare

Stage 2: where

| where SrcGeoCountry in~ (bl_countries)

Stage 3: extend

| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompleteUrlextend

Cross-Cloud Unauthorized Credential Access Detection From AWS RDS Login

#
Severity
medium
Time window
1d
Group by
$left.AWSNetworkEntity, $right.IPAddress, AuthenticationRequirement, ClientAppUsed, ConditionalAccessStatus, IPAddress, OperationName, RiskDetail, UserAgent, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

This detection correlates AWS GuardDuty Credential Access alerts related to Amazon Relational Database Service (RDS) activity with Azure portal sign-in activities. It identifies successful and failed logins, anomalous behavior, and malicious IP access. By joining these datasets on network entities and IP addresses, it detects unauthorized credential access attempts across AWS and Azure resources, enhancing cross-cloud security monitoring.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 122fbc6a-57ab-4aa7-b9a9-51ac4970cac1
name: Cross-Cloud Unauthorized Credential Access Detection From AWS RDS Login 
description: |
  'This detection correlates AWS GuardDuty Credential Access alerts related to Amazon Relational Database Service (RDS) activity with Azure portal sign-in activities. It identifies successful and failed logins, anomalous behavior, and malicious IP access. By joining these datasets on network entities and IP addresses, it detects unauthorized credential access attempts across AWS and Azure resources, enhancing cross-cloud security monitoring.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AWSS3
    dataTypes:
      - AWSGuardDuty
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1557
  - T1110
  - T1110.003
  - T1110.004
  - T1606
  - T1556
  - T1133
query: |
    // Define variable 'AwsAlert' to collect AWS GuardDuty CredentialAccess alerts related to Amazon Relational Database Service (RDS) activity
    let AwsAlert = materialize (
        AWSGuardDuty
        | where ActivityType has_any (
            "CredentialAccess:RDS/TorIPCaller.SuccessfulLogin",
            "CredentialAccess:RDS/TorIPCaller.FailedLogin",
            "CredentialAccess:RDS/AnomalousBehavior.SuccessfulBruteForce",
            "CredentialAccess:RDS/AnomalousBehavior.SuccessfulLogin",
            "CredentialAccess:RDS/MaliciousIPCaller.SuccessfulLogin",
            "CredentialAccess:RDS/MaliciousIPCaller.FailedLogin"
            )
        | extend
            AWSAlertId = Id, 
            AWSAlertTitle = Title,
            AWSAlertDescription = Description,
            AWSresourceType = tostring(parse_json(ResourceDetails).resourceType),
            AWSNetworkEntity = tostring(parse_json(ServiceDetails).action.rdsLoginAttemptAction.remoteIpDetails.ipAddressV4),
            RDSInstanceId = tostring(parse_json(ResourceDetails).rdsDbInstanceDetails.dbInstanceIdentifier),
            RDSUser = tostring(parse_json(ResourceDetails).rdsDbUserDetails.user),
            RDSApplication = tostring(parse_json(ResourceDetails).rdsDbUserDetails.application),
            RDSactionType = tostring(parse_json(ServiceDetails).action.actionType),
            AWSAlertTime = TimeCreated,
            AWSAlertLink= tostring(strcat('https://us-east-1.console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?macros=current&fId=',Id)),
            Severity = 
      case (
        Severity >= 7.0, "High",
        Severity between (4.0 .. 6.9), "Medium",
        Severity between (1.0 .. 3.9), "Low",
        "Unknown")
        | distinct
            AWSAlertTime,
            ActivityType,
            AWSAlertId,
            AWSAlertLink,
            AWSAlertTitle,
            AWSAlertDescription,
            AWSresourceType,
            Arn,
            Severity,
            RDSactionType,
            RDSApplication,
            RDSInstanceId,
            RDSUser,
            AWSNetworkEntity
        );
      // Define variable 'Azure_sigin' to collect Azure portal sign-in activities
      let Azure_sigin = materialize (
          SigninLogs
          | where AppDisplayName == "Azure Portal"
          | where isnotempty(OriginalRequestId)
          | summarize 
              AzureSuccessfulEvent = countif(ResultType == 0), 
              AzureFailedEvent = countif(ResultType != 0), 
              totalAzureLoginEventId = dcount(OriginalRequestId), 
              AzureFailedEventsCount = dcountif(OriginalRequestId, ResultType != 0), 
              AzureSuccessfuleventsCount = dcountif(OriginalRequestId, ResultType == 0),
              AzureSetOfFailedevents = makeset(iff(ResultType != 0, OriginalRequestId, ""), 5), 
              AzureSetOfSuccessfulEvents = makeset(iff(ResultType == 0, OriginalRequestId, ""), 5) 
              by 
              IPAddress, 
              UserPrincipalName, 
              bin(TimeGenerated, 1min), 
              UserAgent,
              ConditionalAccessStatus,
              OperationName,
              RiskDetail,
              AuthenticationRequirement,
              ClientAppUsed
          // Extracting the name and UPN suffix from UserPrincipalName
          | extend
              Name = tostring(split(UserPrincipalName, '@')[0]),
              UPNSuffix = tostring(split(UserPrincipalName, '@')[1])
          );
      // Join 'AwsAlert' and 'Azure_sigin' on the AWS Network Entity and Azure IP Address
      AwsAlert
      | join kind=inner Azure_sigin on $left.AWSNetworkEntity == $right.IPAddress
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
customDetails:
  AWSAlertUserName: RDSUser
  AWSArn: Arn
  AWSresourceType: AWSresourceType
  AWSInstanceType: RDSactionType
  AWSAplicationName: RDSApplication
  AWSInstanceId: RDSInstanceId
  AzureUserAgent: UserAgent
  AzureUser: UserPrincipalName
  AzureClientAppUsed: ClientAppUsed
  AzConditionalAccess: ConditionalAccessStatus
  AzureOperationName: OperationName
  AzureRiskDetail: RiskDetail
  AzAuthRequirement: AuthenticationRequirement
  alertSeverity: Severity
alertDetailsOverride:
  alertDisplayNameFormat: "IP address {{IPAddress}} in {{AWSAlertTitle}} seen in Azure Signin Logs with {{UserPrincipalName}}"
  alertDescriptionFormat: "This detection correlates AWS GuardDuty Credential Access alert described '{{AWSAlertDescription}}' related to Amazon Relational Database Service (RDS) activity with Azure portal sign-in activities. It identifies successful and failed logins, anomalous behavior, and malicious IP access. By joining these datasets on network entities and IP addresses, it detects unauthorized credential access attempts across AWS and Azure resources, enhancing cross-cloud security monitoring. \n\n AWS ALert Link : '{{AWSAlertLink}}' \n\n Find More Details :https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html"
  alertSeverityColumnName: Severity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: AWSAlertLink
    - alertProperty: ProviderName
      value: "AWS"
    - alertProperty: ProductName
      value: "AWSGuardDuty"
    - alertProperty: ProductComponentName
      value: "AWSGuardDuty"
kind: Scheduled
version: 1.0.3

Stages and Predicates

let AwsAlert is inlined into the numbered stages below.

Let Binding: Azure_sigin used in Stage 5

let Azure_sigin = materialize (
      SigninLogs
      | where AppDisplayName == "Azure Portal"
      | where isnotempty(OriginalRequestId)
      | summarize 
          AzureSuccessfulEvent = countif(ResultType == 0), 
          AzureFailedEvent = countif(ResultType != 0), 
          totalAzureLoginEventId = dcount(OriginalRequestId), 
          AzureFailedEventsCount = dcountif(OriginalRequestId, ResultType != 0), 
          AzureSuccessfuleventsCount = dcountif(OriginalRequestId, ResultType == 0),
          AzureSetOfFailedevents = makeset(iff(ResultType != 0, OriginalRequestId, ""), 5), 
          AzureSetOfSuccessfulEvents = makeset(iff(ResultType == 0, OriginalRequestId, ""), 5) 
          by 
          IPAddress, 
          UserPrincipalName, 
          bin(TimeGenerated, 1min), 
          UserAgent,
          ConditionalAccessStatus,
          OperationName,
          RiskDetail,
          AuthenticationRequirement,
          ClientAppUsed
      | extend
          Name = tostring(split(UserPrincipalName, '@')[0]),
          UPNSuffix = tostring(split(UserPrincipalName, '@')[1])
      );

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

Stage 1: source

AWSGuardDuty

Stage 2: where

| where ActivityType has_any (
        "CredentialAccess:RDS/TorIPCaller.SuccessfulLogin",
        "CredentialAccess:RDS/TorIPCaller.FailedLogin",
        "CredentialAccess:RDS/AnomalousBehavior.SuccessfulBruteForce",
        "CredentialAccess:RDS/AnomalousBehavior.SuccessfulLogin",
        "CredentialAccess:RDS/MaliciousIPCaller.SuccessfulLogin",
        "CredentialAccess:RDS/MaliciousIPCaller.FailedLogin"
        )

Stage 3: extend

| extend
        AWSAlertId = Id, 
        AWSAlertTitle = Title,
        AWSAlertDescription = Description,
        AWSresourceType = tostring(parse_json(ResourceDetails).resourceType),
        AWSNetworkEntity = tostring(parse_json(ServiceDetails).action.rdsLoginAttemptAction.remoteIpDetails.ipAddressV4),
        RDSInstanceId = tostring(parse_json(ResourceDetails).rdsDbInstanceDetails.dbInstanceIdentifier),
        RDSUser = tostring(parse_json(ResourceDetails).rdsDbUserDetails.user),
        RDSApplication = tostring(parse_json(ResourceDetails).rdsDbUserDetails.application),
        RDSactionType = tostring(parse_json(ServiceDetails).action.actionType),
        AWSAlertTime = TimeCreated,
        AWSAlertLink= tostring(strcat('https://us-east-1.console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?macros=current&fId=',Id)),
        Severity = 
  case (
    Severity >= 7.0, "High",
    Severity between (4.0 .. 6.9), "Medium",
    Severity between (1.0 .. 3.9), "Low",
    "Unknown")
Severity =
ifSeverity >= 7.0"High"
elifSeverity >= 4.0 and Severity <= 6.9"Medium"
elifSeverity >= 1.0 and Severity <= 3.9"Low"
else"Unknown"

Stage 4: distinct

| distinct
        AWSAlertTime,
        ActivityType,
        AWSAlertId,
        AWSAlertLink,
        AWSAlertTitle,
        AWSAlertDescription,
        AWSresourceType,
        Arn,
        Severity,
        RDSactionType,
        RDSApplication,
        RDSInstanceId,
        RDSUser,
        AWSNetworkEntity

Stage 5: join

AwsAlert
| join kind=inner Azure_sigin on $left.AWSNetworkEntity == $right.IPAddress

Stage 6: summarize aggregation inside the join branch

summarize by IPAddress, UserPrincipalName, UserAgent, ConditionalAccessStatus, OperationName, RiskDetail, AuthenticationRequirement, ClientAppUsed

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityTypematch
  • CredentialAccess:RDS/AnomalousBehavior.SuccessfulBruteForce transforms: term corpus 2 (kusto 2)
  • CredentialAccess:RDS/AnomalousBehavior.SuccessfulLogin transforms: term
  • CredentialAccess:RDS/MaliciousIPCaller.FailedLogin transforms: term
  • CredentialAccess:RDS/MaliciousIPCaller.SuccessfulLogin transforms: term
  • CredentialAccess:RDS/TorIPCaller.FailedLogin transforms: term corpus 2 (kusto 2)
  • CredentialAccess:RDS/TorIPCaller.SuccessfulLogin transforms: term corpus 2 (kusto 2)
field:"ActivityType" kind:match
AppDisplayNameeq
  • Azure Portal
field:"AppDisplayName" kind:eq value:"Azure Portal"
OriginalRequestIdis_not_null
  • (no value, null check)
field:"OriginalRequestId" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthenticationRequirementsummarize
ClientAppUsedsummarize
ConditionalAccessStatussummarize
IPAddresssummarize
OperationNamesummarize
RiskDetailsummarize
UserAgentsummarize
UserPrincipalNamesummarize

CYFIRMA - Brand Intelligence - Malicious Mobile App 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
Source
github.com/Azure/Azure-Sentinel

"This analytic rule detects instances where malicious or unauthorized mobile applications are discovered mimicking legitimate brand assets. Such impersonations may be distributed through unofficial app stores or third-party websites, potentially deceiving customers, harvesting sensitive data, or damaging brand reputation. This alert is triggered when CYFIRMA threat intelligence identifies a suspicious mobile app associated with the organization's brand or product names."

MITRE ATT&CK Coverage

Rule Body

id: 3176ac89-b195-48b7-a01e-740a6b26fb2f
name: CYFIRMA - Brand Intelligence - Malicious Mobile App High Rule
description: |
  "This analytic rule detects instances where malicious or unauthorized mobile applications are discovered mimicking legitimate brand assets. 
  Such impersonations may be distributed through unofficial app stores or third-party websites, potentially deceiving customers, harvesting sensitive data, or damaging brand reputation. 
  This alert is triggered when CYFIRMA threat intelligence identifies a suspicious mobile app associated with the organization's brand or product names."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaBrandIntelligenceAlertsDC
    dataTypes:
      - CyfirmaBIMaliciousMobileAppsAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - ResourceDevelopment
  - Execution
  - DefenseEvasion
  - CredentialAccess
  - CommandAndControl
relevantTechniques:
  - T1406
  - T1414
  - T1437
  - T1583.001
  - T1204.002
query: |
  // High severity - Malicious Mobile App Impersonation
  let timeFrame = 5m;
  CyfirmaBIMaliciousMobileAppsAlerts_CL
  | where severity == 'Critical' 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=asset_value,
      Impact=impact,
      Recommendation=recommendation,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      Recommendation,
      ProductName,
      ProviderName
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  Impact: Impact
  Recommendation: Recommendation
  Description: Description
  RiskScore: RiskScore
  TimeGenerated: TimeGenerated
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Alert: Malicious Mobile App Impersonating Brand Detected - {{AssetType}} - {{AssetValue}} "
  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

CyfirmaBIMaliciousMobileAppsAlerts_CL

Stage 2: where

| where severity == 'Critical' 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=asset_value,
    Impact=impact,
    Recommendation=recommendation,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Description,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    AssetType,
    AssetValue,
    Impact,
    Recommendation,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

Dataverse - Mass download from SharePoint document management

#
Status
available
Severity
low
Time window
14d
Group by
$left.SharePointUrl, $right.Site_Url, ClientIP, InstanceUrl, SharePointUrl, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies mass download (in the last hour) of files from SharePoint sites configured for document management in Dynamics 365. This analytics rule utilizes the MSBizApps-Configuration watchlist to identify SharePoint sites used for Document Management.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

PlatformRecord / event type
Microsoft 365FileDownloaded

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 95e02f1b-5886-4043-8f0e-a42e6e23330f
kind: Scheduled
name: Dataverse - Mass download from SharePoint document management
description: Identifies mass download (in the last hour) of files from SharePoint
  sites configured for document management in Dynamics 365. This analytics rule utilizes
  the MSBizApps-Configuration watchlist to identify SharePoint sites used for Document
  Management.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (SharePoint)
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  // Set threshold for number of downloaded files
  let detection_threshold = 10000;
  let query_frequency = 1h;
  DataverseSharePointSites
  | join kind=inner (
      OfficeActivity
      | where TimeGenerated >= ago(query_frequency)
      | where OfficeWorkload == "SharePoint" and Operation == "FileDownloaded")
      on $left.SharePointUrl == $right.Site_Url
  | summarize FileDownloadCount = count() by UserId, SharePointUrl, InstanceUrl, ClientIP
  | where FileDownloadCount > detection_threshold
  | extend
      CloudAppId = int(32780),
      SharePointId = int(20892),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      UserId,
      ClientIP,
      FileDownloadCount,
      SharePointUrl,
      InstanceUrl,
      CloudAppId,
      SharePointId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: SharePointId
      - identifier: InstanceName
        columnName: SharePointUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Mass download detected from document management
    in {{{InstanceUrl}} '
  alertDescriptionFormat: '{{{FileDownloadCount}} files were downloaded from {{SharePointUrl}}  by
    {{{UserId}}.'
version: 3.2.0

Stages and Predicates

Parameters

let detection_threshold = 10000;
let query_frequency = 1h;

Stage 1: source

DataverseSharePointSites

Stage 2: join

| join kind=inner (
    OfficeActivity
    | where TimeGenerated >= ago(query_frequency)
    | where OfficeWorkload == "SharePoint" and Operation == "FileDownloaded")
    on $left.SharePointUrl == $right.Site_Url

Stage 3: summarize

summarize FileDownloadCount by UserId, SharePointUrl, InstanceUrl, ClientIP

Stage 4: where

where FileDownloadCount > 10000

Stage 5: extend

extend AccountName, CloudAppId, SharePointId, UPNSuffix

Stage 6: project

project AccountName, ClientIP, CloudAppId, FileDownloadCount, InstanceUrl, SharePointId, SharePointUrl, UPNSuffix, UserId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIPproject
CloudAppIdproject
FileDownloadCountproject
InstanceUrlproject
SharePointIdproject
SharePointUrlproject
UPNSuffixproject
UserIdproject

Dataverse - Unusual sign-in following disabled IP address-based cookie binding protection

#

Front Door Premium WAF - XSS Detection

#
Status
available
Severity
high
Time window
6h
Group by
action_s, clientIP_s, host_s, trackingReference_s
Source
github.com/Azure/Azure-Sentinel

'Identifies a match for an XSS attack in the Front Door Premium WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements. References: https://owasp.org/www-project-top-ten/2017/A7_2017-Cross-Site_Scripting_(XSS)'

MITRE ATT&CK Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b7643904-5081-4920-917e-a559ddc3448f
name: Front Door Premium WAF - XSS Detection
description: | 
  'Identifies a match for an XSS attack in the Front Door Premium WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements.
   References: https://owasp.org/www-project-top-ten/2017/A7_2017-Cross-Site_Scripting_(XSS)'
severity: High
status: Available
requiredDataConnectors: 
  - connectorId: WAF 
    dataTypes: 
      - AzureDiagnostics
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - InitialAccess
  - Execution
relevantTechniques:
  - T1189
  - T1203
  - T0853
tags:
  - Cross Site Scripting
query:  |  
    let Threshold = 1;
    AzureDiagnostics
    | where Category =~ "FrontDoorWebApplicationFirewallLog"
    | where action_s =~ "AnomalyScoring"
    | where details_msg_s has "XSS"
    | parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *
    | project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData
    | join kind = inner(
    AzureDiagnostics
    | where Category =~ "FrontDoorWebApplicationFirewallLog"
    | where action_s =~ "Block") on trackingReference_s
    | summarize URI_s = make_set(requestUri_s,100), Table = make_set(TableName,100), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), TrackingReference = make_set(trackingReference_s,100), Matched_Data = make_set(MatchedData,100), Detail_Data = make_set(details_data_s,100), Detail_Message = make_set(details_msg_s,100), Total_TrackingReference = dcount(trackingReference_s) by clientIP_s, host_s, action_s
    | where Total_TrackingReference >= Threshold
# The Threshold value above can be changed as per your infrastructure's requirement 
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URI_s
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: clientIP_s 
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let Threshold = 1;

Stage 1: source

AzureDiagnostics

Stage 2: where

| where Category =~ "FrontDoorWebApplicationFirewallLog"

Stage 3: where

| where action_s =~ "AnomalyScoring"

Stage 4: where

| where details_msg_s has "XSS"

Stage 5: parse

| parse details_data_s with MessageText "Matched Data:" MatchedData "AND " * "table_name FROM " TableName " " *

Stage 6: project

| project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s, TableName, MatchedData

Stage 7: join

| join kind = inner(
AzureDiagnostics
| where Category =~ "FrontDoorWebApplicationFirewallLog"
| where action_s =~ "Block") on trackingReference_s

Stage 8: summarize

summarize Detail_Data, Detail_Message, EndTime, Matched_Data, StartTime, Table, Total_TrackingReference, TrackingReference, URI_s by clientIP_s, host_s, action_s

Stage 9: where

where Total_TrackingReference >= 1

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Detail_Datasummarize
Detail_Messagesummarize
EndTimesummarize
Matched_Datasummarize
StartTimesummarize
Tablesummarize
Total_TrackingReferencesummarize
TrackingReferencesummarize
URI_ssummarize
action_ssummarize
clientIP_ssummarize
host_ssummarize

GitLab - Personal Access Tokens creation over time

#
Status
available
Severity
medium
Time window
1d
Group by
AuthorName, Repository, TimeGenerated
Source
github.com/Azure/Azure-Sentinel

'This queries GitLab Audit Logs for access tokens. Attacker can exfiltrate data from you GitLab repository after gaining access to it by generating or hijacking access tokens. This hunting queries allows you to track the personal access tokens creation for each of your repositories. The visualization allow you to quickly identify anomalies/excessive creation, to further investigate repo access & permissions.'

MITRE ATT&CK Coverage

Rule Body

id: 4d6d8b0e-6d9a-4857-a141-f5d89393cddb
name: GitLab - Personal Access Tokens creation over time
description: |
  'This queries GitLab Audit Logs for access tokens. Attacker can exfiltrate data from you GitLab repository after gaining access to it by generating or hijacking access tokens. 
  This hunting queries allows you to track the personal access tokens creation for each of your repositories. 
  The visualization allow you to quickly identify anomalies/excessive creation, to further investigate repo access & permissions.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
query: |
  // l_min_tokens_created - minimum tokens created per repository per user per day to consider
  let l_min_tokens_created = 0;
  let interval = 1d;
  GitLabAudit
  | where TargetType == "PersonalAccessToken"
  | project Severity, TimeGenerated = bin(todatetime(EventTime),1d), AuthorName, IPAddress, Repository = EntityName, Action, TargetType
  | summarize total = count() by Repository, TimeGenerated, AuthorName
  | where total >= l_min_tokens_created
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AuthorName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let l_min_tokens_created = 0;
let interval = 1d;

Stage 1: source

GitLabAudit

Stage 2: where

| where TargetType == "PersonalAccessToken"

Stage 3: project

| project Severity, TimeGenerated = bin(todatetime(EventTime),1d), AuthorName, IPAddress, Repository = EntityName, Action, TargetType

Stage 4: summarize

| summarize total = count() by Repository, TimeGenerated, AuthorName

Stage 5: where

| where total >= l_min_tokens_created

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthorNamesummarize
Repositorysummarize
TimeGeneratedsummarize
totalsummarize

Idira - High-Risk Actions Outside Business Hours

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

Detects privileged or destructive actions (delete/disable/rotate/elevate/etc.) occurring outside standard business hours. Useful for insider misuse or compromised admin detection.

MITRE ATT&CK Coverage

TacticTechniques
StealthNo specific technique

Rule Body

id: 3733e1ac-991b-4504-99e8-24ff4fbaf6e4
kind: Scheduled
name: Idira - High-Risk Actions Outside Business Hours
description: >
  Detects privileged or destructive actions (delete/disable/rotate/elevate/etc.)
  occurring outside standard business hours. Useful for insider misuse or
  compromised admin detection.
severity: High
tactics:
  - DefenseEvasion
queryFrequency: 10M
queryPeriod: 1D
triggerOperator: GreaterThan
triggerThreshold: 0
query: |
  let risky = dynamic(["Delete","Remove","Rotate","Elevate","Disable","Grant","Policy","Safe","Vault","Key"]);
  CyberArk_AuditEvents_CL
  | where isnotempty(action) or isnotempty(actionType) or isnotempty(auditType) or isnotempty(message)
  | where hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) > 20
  | where action has_any (risky) or actionType has_any (risky) or auditType has_any (risky) or message has_any (risky)
  | extend cd = parse_json(customData)
  | extend cd_username = tostring(cd.username),
           cd_auth_method = tostring(cd.authentication_method),
           cd_client_ip = tostring(cd.client_ip_address),
           cd_source_ip = tostring(cd.source_ip_address),
           cd_device_os = tostring(cd.device_os),
           cd_browser = tostring(cd.browser_name),
           cd_geo_city = tostring(cd.geoip_city_name),
           cd_geo_country = tostring(cd.geoip_country_name),
           cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))
  | project TimeGenerated, CyberArkTenantId, username, cd_username, identityType, action, actionType, auditType,
            target, targetAccount, safe, source, cd_source_ip, cd_client_ip, cd_auth_method, cd_device_os,
            cd_browser, cd_geo_city, cd_geo_country, cd_target, component, serviceName, message

entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: username
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: target
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: source
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Parameters

let risky = dynamic(["Delete","Remove","Rotate","Elevate","Disable","Grant","Policy","Safe","Vault","Key"]);

Stage 1: source

CyberArk_AuditEvents_CL

Stage 2: where

| where isnotempty(action) or isnotempty(actionType) or isnotempty(auditType) or isnotempty(message)

Stage 3: where

| where hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) > 20

Stage 4: where

| where action has_any (risky) or actionType has_any (risky) or auditType has_any (risky) or message has_any (risky)

Stage 5: extend

| extend cd = parse_json(customData)

Stage 6: extend

| extend cd_username = tostring(cd.username),
         cd_auth_method = tostring(cd.authentication_method),
         cd_client_ip = tostring(cd.client_ip_address),
         cd_source_ip = tostring(cd.source_ip_address),
         cd_device_os = tostring(cd.device_os),
         cd_browser = tostring(cd.browser_name),
         cd_geo_city = tostring(cd.geoip_city_name),
         cd_geo_country = tostring(cd.geoip_country_name),
         cd_target = coalesce(tostring(cd.target), tostring(cd.target_resource), tostring(cd.new_target))

Stage 7: project

| project TimeGenerated, CyberArkTenantId, username, cd_username, identityType, action, actionType, auditType,
          target, targetAccount, safe, source, cd_source_ip, cd_client_ip, cd_auth_method, cd_device_os,
          cd_browser, cd_geo_city, cd_geo_country, cd_target, component, serviceName, message

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
TimeGeneratedgt
  • 20 transforms: hourofday
field:"TimeGenerated" kind:gt value:"20"
TimeGeneratedlt
  • 7 transforms: hourofday
field:"TimeGenerated" kind:lt value:"7"
actionis_not_null
  • (no value, null check)
field:"action" kind:is_not_null
actionmatch
  • Delete transforms: term
  • Disable transforms: term
  • Elevate transforms: term
  • Grant transforms: term
  • Key transforms: term
  • Policy transforms: term
  • Remove transforms: term
  • Rotate transforms: term
  • Safe transforms: term
  • Vault transforms: term
field:"action" kind:match
actionTypeis_not_null
  • (no value, null check)
field:"actionType" kind:is_not_null
actionTypematch
  • Delete transforms: term
  • Disable transforms: term
  • Elevate transforms: term
  • Grant transforms: term
  • Key transforms: term
  • Policy transforms: term
  • Remove transforms: term
  • Rotate transforms: term
  • Safe transforms: term
  • Vault transforms: term
field:"actionType" kind:match
auditTypeis_not_null
  • (no value, null check)
field:"auditType" kind:is_not_null
auditTypematch
  • Delete transforms: term
  • Disable transforms: term
  • Elevate transforms: term
  • Grant transforms: term
  • Key transforms: term
  • Policy transforms: term
  • Remove transforms: term
  • Rotate transforms: term
  • Safe transforms: term
  • Vault transforms: term
field:"auditType" kind:match
messageis_not_null
  • (no value, null check)
field:"message" kind:is_not_null
messagematch
  • Delete transforms: term
  • Disable transforms: term
  • Elevate transforms: term
  • Grant transforms: term
  • Key transforms: term
  • Policy transforms: term
  • Remove transforms: term
  • Rotate transforms: term
  • Safe transforms: term
  • Vault transforms: term
field:"message" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
CyberArkTenantIdproject
TimeGeneratedproject
actionproject
actionTypeproject
auditTypeproject
cd_auth_methodproject
cd_browserproject
cd_client_ipproject
cd_device_osproject
cd_geo_cityproject
cd_geo_countryproject
cd_source_ipproject
cd_targetproject
cd_usernameproject
componentproject
identityTypeproject
messageproject
safeproject
serviceNameproject
sourceproject
targetproject
targetAccountproject
usernameproject

Infoblox - Many NXDOMAIN DNS Responses Detected

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

'Detected at least 200 DNS responses for non-existent domains in 1 hour generated by single host. Queries do not need to be the same. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxCDC.'

MITRE ATT&CK Coverage

Rule Body

id: b2f34315-9065-488e-88d0-a171d2b0da8e
name: Infoblox - Many NXDOMAIN DNS Responses Detected
description: |
  'Detected at least 200 DNS responses for non-existent domains in 1 hour generated by single host. Queries do not need to be the same. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxCDC**](https://aka.ms/sentinel-InfobloxCloudDataConnector-parser).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1565
query: |
  let threshold = 200;
  InfobloxCDC
  | where DeviceEventClassID == "DNS Response"
  | where InfobloxDNSRCode == "NXDOMAIN"
  | summarize count() by SourceIP
  | where count_ > threshold
  | join kind=inner (InfobloxCDC
    | where DeviceEventClassID == "DNS Response"
    | where InfobloxDNSRCode == "NXDOMAIN"
    ) on SourceIP
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
      - identifier: OSVersion
        columnName: InfobloxB1SrcOSVersion
      - identifier: FullName
        columnName: SourceUserName
customDetails:
  SourceMACAddress: SourceMACAddress
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 200;

Stage 1: source

InfobloxCDC

Stage 2: where

| where DeviceEventClassID == "DNS Response"

Stage 3: where

| where InfobloxDNSRCode == "NXDOMAIN"

Stage 4: summarize

| summarize count() by SourceIP

Stage 5: where

| where count_ > threshold

Stage 6: join

| join kind=inner (InfobloxCDC
  | where DeviceEventClassID == "DNS Response"
  | where InfobloxDNSRCode == "NXDOMAIN"
  ) on SourceIP

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIPsummarize

Lumen TI IPAddress in CommonSecurityLog

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.CS_ipEntity, CS_ipEntity, Id
Source
github.com/Azure/Azure-Sentinel

This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: bc8a262a-5db3-4ac1-8757-519ed36ed929
name: Lumen TI IPAddress in CommonSecurityLog
displayName: Lumen TI IPAddress in CommonSecurityLog
description: |
  This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in CommonSecurityLog.
severity: Medium
requiredDataConnectors:
  - connectorId: LumenThreatFeedConnector
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceUploadIndicatorsAPI
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: CEF
    dataTypes:
      - CommonSecurityLog
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
  let dt_lookBack = 1h; // Look back 1 hour for CommonSecurityLog events
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelIndicators
   | where TimeGenerated >= ago(ioc_lookBack)
   | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
   | where IsActive == true and ValidUntil > now()
   | where SourceSystem == 'Lumen'
   | where ObservableKey == "ipv4-addr:value"
   | extend TI_ipEntity = ObservableValue
   | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127.";
  // Perform a join between IP indicators and CommonSecurityLog events
  IP_Indicators
   | join kind=innerunique (
       CommonSecurityLog
       | where TimeGenerated >= ago(dt_lookBack)
       | extend MessageIP = extract(IPRegex, 0, Message)
       | extend CS_ipEntity = iff(isnotempty(SourceIP), SourceIP, DestinationIP)
       | extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
       | extend CommonSecurityLog_TimeGenerated = TimeGenerated
   )
   on $left.TI_ipEntity == $right.CS_ipEntity
   | where CommonSecurityLog_TimeGenerated < ValidUntil
   | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id, CS_ipEntity
   | project timestamp = CommonSecurityLog_TimeGenerated, SourceIP, DestinationIP, MessageIP, Message, DeviceVendor, DeviceProduct, Id, Tags, ValidUntil, Confidence, TI_ipEntity, CS_ipEntity, LogSeverity, DeviceAction, Type
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CS_ipEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 8 define let IP_Indicators as the rule's main pipeline source. Stages 9 to 12 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: summarize

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

Stage 4: where

| where IsActive == true and ValidUntil > now()

Stage 5: where

| where SourceSystem == 'Lumen'

Stage 6: where

| where ObservableKey == "ipv4-addr:value"

Stage 7: extend

| extend TI_ipEntity = ObservableValue

Stage 8: where

| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 9: join

IP_Indicators
| join kind=innerunique (
     CommonSecurityLog
     | where TimeGenerated >= ago(dt_lookBack)
     | extend MessageIP = extract(IPRegex, 0, Message)
     | extend CS_ipEntity = iff(isnotempty(SourceIP), SourceIP, DestinationIP)
     | extend CS_ipEntity = iff(isempty(CS_ipEntity) and isnotempty(MessageIP), MessageIP, CS_ipEntity)
     | extend CommonSecurityLog_TimeGenerated = TimeGenerated
 )
 on $left.TI_ipEntity == $right.CS_ipEntity

Stage 10: where

where CommonSecurityLog_TimeGenerated < ValidUntil

Stage 11: summarize

summarize CommonSecurityLog_TimeGenerated by Id, CS_ipEntity

Stage 12: project

project CS_ipEntity, Confidence, DestinationIP, DeviceAction, DeviceProduct, DeviceVendor, Id, LogSeverity, Message, MessageIP, SourceIP, TI_ipEntity, Tags, Type, ValidUntil, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CS_ipEntityproject
Confidenceproject
DestinationIPproject
DeviceActionproject
DeviceProductproject
DeviceVendorproject
Idproject
LogSeverityproject
Messageproject
MessageIPproject
SourceIPproject
TI_ipEntityproject
Tagsproject
Typeproject
ValidUntilproject
timestampproject

Malicious Inbox Rule

#
Status
available
Severity
medium
Time window
1d
Group by
ClientIPAddress, Keyword, OfficeObjectId, Operation, OriginatingServer, ResultStatus, RuleDetail, UserId
Source
github.com/Azure/Azure-Sentinel

Often times after the initial compromise the attackers create inbox rules to delete emails that contain certain keywords. This is done so as to limit ability to warn compromised users that they've been compromised. Below is a sample query that tries to detect this. Reference: https://www.reddit.com/r/sysadmin/comments/7kyp0a/recent_phishing_attempts_my_experience_and_what/

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 7b907bf7-77d4-41d0-a208-5643ff75bf9a
name: Malicious Inbox Rule
description: |
  'Often times after the initial compromise the attackers create inbox rules to delete emails that contain certain keywords.
   This is done so as to limit ability to warn compromised users that they've been compromised. Below is a sample query that tries to detect this.
  Reference: https://www.reddit.com/r/sysadmin/comments/7kyp0a/recent_phishing_attempts_my_experience_and_what/'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1098
  - T1078
query: |
 let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);
 OfficeActivity
 | where OfficeWorkload =~ "Exchange" 
 | where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded")
 | where Parameters has "Deleted Items" or Parameters has "Junk Email"  or Parameters has "DeleteMessage"
 | extend Events=todynamic(Parameters)
 | parse Events  with * "SubjectContainsWords" SubjectContainsWords '}'*
 | parse Events  with * "BodyContainsWords" BodyContainsWords '}'*
 | parse Events  with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
 | where SubjectContainsWords has_any (Keywords)
  or BodyContainsWords has_any (Keywords)
  or SubjectOrBodyContainsWords has_any (Keywords)
 | extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
 | extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
 | extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
 | summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by  Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
 | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
 | extend OriginatingServerName = tostring(split(OriginatingServer, " ")[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: OriginatingServerName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIPAddress
version: 2.0.4
kind: Scheduled

Stages and Predicates

Parameters

let Keywords = dynamic(["helpdesk", " alert", " suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]);

Stage 1: source

OfficeActivity

Stage 2: where

| where OfficeWorkload =~ "Exchange"

Stage 3: where

| where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded")

Stage 4: where

| where Parameters has "Deleted Items" or Parameters has "Junk Email"  or Parameters has "DeleteMessage"

Stage 5: extend

| extend Events=todynamic(Parameters)

Stage 6: parse

| parse Events  with * "SubjectContainsWords" SubjectContainsWords '}'*

Stage 7: parse

| parse Events  with * "BodyContainsWords" BodyContainsWords '}'*

Stage 8: parse

| parse Events  with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*

Stage 9: where

| where SubjectContainsWords has_any (Keywords)
 or BodyContainsWords has_any (Keywords)
 or SubjectOrBodyContainsWords has_any (Keywords)

Stage 10: extend (3 consecutive steps)

| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
ClientIPAddress =
ifClientIP has "."tostring(split(ClientIP, ":")[0])
elifClientIP has "["tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0])))
elseClientIP

Stage 11: summarize

| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by  Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail

Stage 12: extend

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

Stage 13: extend

| extend OriginatingServerName = tostring(split(OriginatingServer, " ")[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
BodyContainsWordsmatch
  • alert transforms: term corpus 2 (kusto 2)
  • suspicious transforms: term corpus 2 (kusto 2)
  • Fatal transforms: term corpus 2 (kusto 2)
  • do not click transforms: term corpus 2 (kusto 2)
  • do not open transforms: term corpus 2 (kusto 2)
  • fake transforms: term corpus 2 (kusto 2)
  • helpdesk transforms: term corpus 2 (kusto 2)
  • hijacked transforms: term corpus 2 (kusto 2)
  • malicious transforms: term corpus 2 (kusto 2)
  • phishing transforms: term corpus 3 (kusto 3)
  • spam transforms: term corpus 2 (kusto 2)
field:"BodyContainsWords" kind:match
OfficeWorkloadeq
  • Exchange corpus 6 (kusto 6)
field:"m365::Workload" kind:eq value:"Exchange"
Operationeq
  • New-InboxRule corpus 5 (splunk 2, kusto 2, sigma 1)
field:"Operation" kind:eq value:"New-InboxRule"
Parametersmatch
  • DeleteMessage transforms: term corpus 3 (kusto 3)
  • Deleted Items transforms: term corpus 3 (kusto 3)
  • Junk Email transforms: term corpus 3 (kusto 3)
field:"m365::Parameters" kind:match
ResultStatuseq
  • Succeeded corpus 2 (kusto 2)
  • True
field:"ResultStatus" kind:eq
SubjectContainsWordsmatch
  • alert transforms: term corpus 2 (kusto 2)
  • suspicious transforms: term corpus 2 (kusto 2)
  • Fatal transforms: term corpus 2 (kusto 2)
  • do not click transforms: term corpus 2 (kusto 2)
  • do not open transforms: term corpus 2 (kusto 2)
  • fake transforms: term corpus 2 (kusto 2)
  • helpdesk transforms: term corpus 2 (kusto 2)
  • hijacked transforms: term corpus 2 (kusto 2)
  • malicious transforms: term corpus 2 (kusto 2)
  • phishing transforms: term corpus 3 (kusto 3)
  • spam transforms: term corpus 2 (kusto 2)
field:"SubjectContainsWords" kind:match
SubjectOrBodyContainsWordsmatch
  • alert transforms: term corpus 2 (kusto 2)
  • suspicious transforms: term corpus 2 (kusto 2)
  • Fatal transforms: term corpus 2 (kusto 2)
  • do not click transforms: term corpus 2 (kusto 2)
  • do not open transforms: term corpus 2 (kusto 2)
  • fake transforms: term corpus 2 (kusto 2)
  • helpdesk transforms: term corpus 2 (kusto 2)
  • hijacked transforms: term corpus 2 (kusto 2)
  • malicious transforms: term corpus 2 (kusto 2)
  • phishing transforms: term corpus 3 (kusto 3)
  • spam transforms: term corpus 2 (kusto 2)
field:"SubjectOrBodyContainsWords" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPAddresssummarize
EndTimeUtcsummarize
Keywordsummarize
OfficeObjectIdsummarize
Operationsummarize
OriginatingServersummarize
ResultStatussummarize
RuleDetailsummarize
StartTimeUtcsummarize
UserIdsummarize
AccountNameextend
AccountUPNSuffixextend
OriginatingServerNameextend

Netskope - High Severity Alert

#
Status
available
Severity
high
Time window
1h
Group by
AlertName, AlertType, Hostname, NormalizedSeverity, SrcCountry, User, Userip
Source
github.com/Azure/Azure-Sentinel

Detects Netskope alerts raised with a high or critical severity. High severity alerts typically indicate DLP violations, malware detections, compromised credentials, or significant policy breaches that warrant immediate investigation.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Exfiltration

Rule Body

id: a1f6c2d4-8b35-4e19-9c7a-2d4e6f8a1b30
name: Netskope - High Severity Alert
description: |
  Detects Netskope alerts raised with a high or critical severity. High severity alerts
  typically indicate DLP violations, malware detections, compromised credentials, or
  significant policy breaches that warrant immediate investigation.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: NetskopeAlertEventsConnector
    dataTypes:
      - NetskopeAlertEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1078
  - T1567
query: |
  NetskopeAlertEvents_CL
  | where TimeGenerated > ago(1h)
  | where Alert =~ "yes"
  | where Severity in~ ("high", "critical")
  | extend NormalizedSeverity = tolower(Severity)
  | summarize
      AlertCount = count(),
      Activities = make_set(Activity, 20),
      Applications = make_set(App, 20),
      Policies = make_set(Policy, 20),
      Actions = make_set(Action, 10),
      FirstSeen = min(TimeGenerated),
      LastSeen = max(TimeGenerated)
      by AlertName, AlertType, NormalizedSeverity, User, Userip, Hostname, SrcCountry
  | order by AlertCount desc
  | project
      LastSeen,
      AlertName,
      AlertType,
      Severity = NormalizedSeverity,
      User,
      Userip,
      Hostname,
      SrcCountry,
      AlertCount,
      Applications,
      Activities,
      Policies,
      Actions,
      FirstSeen
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: User
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Userip
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

NetskopeAlertEvents_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where Alert =~ "yes"

Stage 4: where

| where Severity in~ ("high", "critical")

Stage 5: extend

| extend NormalizedSeverity = tolower(Severity)

Stage 6: summarize

| summarize
    AlertCount = count(),
    Activities = make_set(Activity, 20),
    Applications = make_set(App, 20),
    Policies = make_set(Policy, 20),
    Actions = make_set(Action, 10),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by AlertName, AlertType, NormalizedSeverity, User, Userip, Hostname, SrcCountry

Stage 7: sort

| order by AlertCount desc

Stage 8: project

| project
    LastSeen,
    AlertName,
    AlertType,
    Severity = NormalizedSeverity,
    User,
    Userip,
    Hostname,
    SrcCountry,
    AlertCount,
    Applications,
    Activities,
    Policies,
    Actions,
    FirstSeen

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionsproject
Activitiesproject
AlertCountproject
AlertNameproject
AlertTypeproject
Applicationsproject
FirstSeenproject
Hostnameproject
LastSeenproject
Policiesproject
Severityproject
SrcCountryproject
Userproject
Useripproject

PaloAlto - Possible port scan

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

'Detects possible port scan.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: 3575a9c0-51c9-11ec-bf63-0242ac130002
name: PaloAlto - Possible port scan
description: |
  'Detects possible port scan.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
query: |
  let threshold = 10;
  PaloAltoCDLEvent
  | where EventResourceId =~ 'TRAFFIC'
  | where isnotempty(DstPortNumber) and isnotempty(SrcIpAddr)
  | summarize PortSet = make_set(DstPortNumber) by SrcIpAddr, bin(TimeGenerated, 5m)
  | where array_length(PortSet) > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

PaloAltoCDLEvent

Stage 2: where

| where EventResourceId =~ 'TRAFFIC'

Stage 3: where

| where isnotempty(DstPortNumber) and isnotempty(SrcIpAddr)

Stage 4: summarize

| summarize PortSet = make_set(DstPortNumber) by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 5: where

| where array_length(PortSet) > threshold

Stage 6: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
PortSetsummarize
SrcIpAddrsummarize
IPCustomEntityextend

Pathlock TDnR - J2EE Security Events

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

Detects security events from SAP J2EE (Java) engine security logs, forwarded by Pathlock Threat Detection and Response. J2EE security anomalies may indicate web application attacks, unauthorized access to SAP NetWeaver Java services, or exploitation of Java-based SAP components.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c35
name: Pathlock TDnR - J2EE Security Events
kind: Scheduled
description: >-
  Detects security events from SAP J2EE (Java) engine security logs, forwarded by Pathlock Threat
  Detection and Response. J2EE security anomalies may indicate web application attacks, unauthorized
  access to SAP NetWeaver Java services, or exploitation of Java-based SAP components.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Discovery
relevantTechniques:
  - T1190
  - T1082
query: |
  Pathlock_TDnR_CL
  | where DataSource == "J2EE_SECURITY_LOG"
  | 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 == "J2EE_SECURITY_LOG"

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

Pathlock TDnR - SU24 Table USOBX_C Changes

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

Detects changes to the SU24 authorization check table USOBX_C in SAP, forwarded by Pathlock Threat Detection and Response. USOBX_C controls whether authorization checks are active for transactions; unauthorized changes may disable security checks, enabling unauthorized access.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c19
name: Pathlock TDnR - SU24 Table USOBX_C Changes
kind: Scheduled
description: >-
  Detects changes to the SU24 authorization check table USOBX_C in SAP, forwarded by Pathlock Threat
  Detection and Response. USOBX_C controls whether authorization checks are active for transactions;
  unauthorized changes may disable security checks, enabling unauthorized access.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
relevantTechniques:
  - T1562
  - T1548
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CHANGEDOC_USOBX_C"
  | 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 == "CHANGEDOC_USOBX_C"

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

Powershell Empire Cmdlets Executed in Command Line

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

This query identifies use of PowerShell Empire's cmdlets within the command line data of the PowerShell process, indicating potential use of the post-exploitation tool.

MITRE ATT&CK Coverage

TacticTechniques
Execution
Persistence
Privilege Escalation
Stealth
Credential Access
Discovery
Lateral Movement
Collection
Command & Control
Exfiltration

Telemetry Coverage

Rule Body

id: ef88eb96-861c-43a0-ab16-f3835a97c928
name: Powershell Empire Cmdlets Executed in Command Line
description: |
  'This query identifies use of PowerShell Empire's cmdlets within the command line data of the PowerShell process, indicating potential use of the post-exploitation tool.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Execution
  - Exfiltration
  - LateralMovement
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1548.002
  - T1134
  - T1134.002
  - T1134.005
  - T1087.001
  - T1087.002
  - T1557.001
  - T1071.001
  - T1560
  - T1547.001
  - T1547.005
  - T1547.009
  - T1217
  - T1115
  - T1059
  - T1059.001
  - T1059.003
  - T1136.001
  - T1136.002
  - T1543.003
  - T1555.003
  - T1484.001
  - T1482
  - T1114.001
  - T1573.002
  - T1546.008
  - T1041
  - T1567.001
  - T1567.002
  - T1068
  - T1210
  - T1083
  - T1615
  - T1574.001
  - T1574.004
  - T1574.007
  - T1574.008
  - T1574.009
  - T1070.006
  - T1105
  - T1056.001
  - T1056.004
  - T1106
  - T1046
  - T1135
  - T1040
  - T1027
  - T1003.001
  - T1057
  - T1055
  - T1021.003
  - T1021.004
  - T1053.005
  - T1113
  - T1518.001
  - T1558.002
  - T1558.003
  - T1082
  - T1016
  - T1049
  - T1569.002
  - T1127.001
  - T1552.001
  - T1552.004
  - T1550.002
  - T1125
  - T1102.002
  - T1047
query: |
  let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));
  (union isfuzzy=true
   (SecurityEvent
  | where EventID == 4688
  //consider filtering on filename if perf issues occur
  //where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
  | where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
  | where CommandLine has "-encodedCommand"
  | parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
  | extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
  // Note: currently the base64_decode_tostring function is limited to supporting UTF8
  | extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) -  (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
  | extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
  | where EfectiveCommand matches regex regexEmpire
  | project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
  | extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
  ),
  (WindowsEvent
  | where EventID == 4688
  | where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
  | where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
  //consider filtering on filename if perf issues occur
  //extend NewProcessName = tostring(EventData.NewProcessName)
  //extend Process=tostring(split(NewProcessName, '\\')[-1])
  //FileName = Process
  //where FileName in~ ("powershell.exe","powershell_ise.exe","pwsh.exe")
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
  | extend CommandLine = tostring(EventData.CommandLine)
  | where CommandLine has "-encodedCommand"
  | parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
  | extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
  // Note: currently the base64_decode_tostring function is limited to supporting UTF8
  | extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) -  (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
  | extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
  | where EfectiveCommand matches regex regexEmpire
  | extend SubjectUserName = tostring(EventData.SubjectUserName)
  | extend SubjectDomainName = tostring(EventData.SubjectDomainName)
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend Process=tostring(split(NewProcessName, '\\')[-1])
  | project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
  | extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
  ))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.3.1
kind: Scheduled

Stages and Predicates

Let Binding: regexEmpire

let regexEmpire = tostring(toscalar(externaldata(cmdlets:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/EmpireCommandString.txt"] with (format="txt")));

Union isfuzzy=true (2 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
| where EventID == 4688
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) -  (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')

Leg 2: WindowsEvent

WindowsEvent
| where EventID == 4688
| where EventData has_any ("-encodedCommand", "powershell.exe","powershell_ise.exe","pwsh.exe")
| where not(EventData has_any ('gc_worker.exe', 'gc_service.exe'))
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where not(ParentProcessName has_any ('gc_worker.exe', 'gc_service.exe'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has "-encodedCommand"
| parse kind=regex flags=i CommandLine with * "-EncodedCommand " encodedCommand
| extend encodedCommand = iff(encodedCommand has " ", tostring(split(encodedCommand, " ")[0]), encodedCommand)
| extend decodedCommand = translate('\0','', base64_decode_tostring(substring(encodedCommand, 0, strlen(encodedCommand) -  (strlen(encodedCommand) %8)))), encodedCommand, CommandLine , strlen(encodedCommand)
| extend EfectiveCommand = iff(isnotempty(encodedCommand), decodedCommand, CommandLine)
| where EfectiveCommand matches regex regexEmpire
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| project timestamp = TimeGenerated, Computer, SubjectUserName, SubjectDomainName, FileName = Process, EfectiveCommand, decodedCommand, encodedCommand, CommandLine, ParentProcessName
| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • -encodedCommand transforms: term
field:"CommandLine" kind:match value:"-encodedCommand"
EfectiveCommandregex_match
  • regexEmpire
field:"EfectiveCommand" kind:regex_match value:"regexEmpire"
EventDatamatch
  • -encodedCommand transforms: term
  • powershell.exe transforms: term
  • powershell_ise.exe transforms: term
  • pwsh.exe transforms: term
field:"EventData" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLineproject
Computerproject
EfectiveCommandproject
FileNameproject
ParentProcessNameproject
SubjectDomainNameproject
SubjectUserNameproject
decodedCommandproject
encodedCommandproject
timestampproject
DnsDomainextend
HostNameextend

SAP ETD - Synch alerts

#

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
2d
Group by
AlertId
Source
github.com/Azure/Azure-Sentinel

Synch alerts coming in from SAP Enterprise Threat Detection into Microsoft Sentinel (one way)

Rule Body

id: 7a830484-e349-4527-85f6-7850c468c238
kind: Scheduled
name: SAP ETD - Synch alerts
description: Synch alerts coming in from SAP Enterprise Threat Detection into Microsoft Sentinel (one way)
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPETDAlerts
    dataTypes:
      - SAPETDAlerts_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  let minThreshold= 1;
  let minScore= 50;
  let lookBack= 7d;
  let regex_sid = @"^([A-Z0-9]{3})/";
  let regex_client = @'\/(.{3})$';
  SAPETDAlerts_CL
  | mv-expand NormalizedTriggeringEvents
  | summarize arg_max(TimeGenerated, *) by AlertId
  | where Threshold >= minThreshold and Score >= minScore
  | extend
    SystemId= extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
    ClienId= extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
    Host= NormalizedTriggeringEvents.NetworkHostnameInitiator,
    Instance= NormalizedTriggeringEvents.NetworkHostnameActor,
    User= NormalizedTriggeringEvents.UserAccountActing,
    IP= NormalizedTriggeringEvents.NetworkIPAddressInitiator
  | mv-expand Users
  | extend
    UserAccountName = tostring(Users.UserAccountName),
    UserEmail = tostring(Users.EmailAddresses[0]);
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserAccountName
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: UserEmail
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: SystemId
      - identifier: AppId
        columnName: ClienId
      - identifier: InstanceName
        columnName: Instance
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Host
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP ETD - {{PatternName}} '
  alertDescriptionFormat: '{{PatternDescription}}'
customDetails:
  SAP_User: User
  SAP_UserEmail: UserEmail
  ETD_AlertNumber: AlertId
version: 1.0.4

Stages and Predicates

Parameters

let minThreshold = 1;
let minScore = 50;
let lookBack = 7d;
let regex_sid = @"^([A-Z0-9]{3})/";
let regex_client = @'\/(.{3})$';

Stage 1: source

SAPETDAlerts_CL

Stage 2: mv-expand

| mv-expand NormalizedTriggeringEvents

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by AlertId

Stage 4: where

| where Threshold >= minThreshold and Score >= minScore

Stage 5: extend

| extend
  SystemId= extract(regex_sid, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
  ClienId= extract(regex_client, 1, tostring(NormalizedTriggeringEvents.SystemIdActor)),
  Host= NormalizedTriggeringEvents.NetworkHostnameInitiator,
  Instance= NormalizedTriggeringEvents.NetworkHostnameActor,
  User= NormalizedTriggeringEvents.UserAccountActing,
  IP= NormalizedTriggeringEvents.NetworkIPAddressInitiator

Stage 6: mv-expand

| mv-expand Users

Stage 7: extend

| extend
  UserAccountName = tostring(Users.UserAccountName),
  UserEmail = tostring(Users.EmailAddresses[0]);

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Scorege
  • 50 transforms: number
field:"Score" kind:ge value:"50"
Thresholdge
  • 1 transforms: number
field:"Threshold" kind:ge value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertIdsummarize
ClienIdextend
Hostextend
IPextend
Instanceextend
SystemIdextend
Userextend
UserAccountNameextend
UserEmailextend

Semperis DSP Kerberos krbtgt account with old password

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

'The krbtgt user account is a special (disabled) user account in every Active Directory domain that has a special role in Kerberos function. If this account's password is compromised, Golden Ticket attacks can be performed to get access to any resource in the AD domain. This indicator looks for a krbtgt user account whose password hasn't been changed in the past 180 days. While Microsoft recommends changing the password every year, STIG recommends changing it every 180 days.'

MITRE ATT&CK Coverage

Rule Body

id: 9ff3b26b-7636-412e-ac46-072b084b94cb
name: Semperis DSP Kerberos krbtgt account with old password
description: |
  'The krbtgt user account is a special (disabled) user account in every Active Directory domain that has a special role in Kerberos function. If this account's password is compromised, Golden Ticket attacks can be performed to get access to any resource in the AD domain. This indicator looks for a krbtgt user account whose password hasn't been changed in the past 180 days. While Microsoft recommends changing the password every year, STIG recommends changing it every 180 days.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SemperisDSP
    dataTypes:
      - dsp_parser
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1558.001
query: |
  dsp_parser
  | where EventID == 9212
  | where SecurityIndicatorName == "Kerberos krbtgt account with old password"
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 2.0.7
kind: Scheduled

Stages and Predicates

Stage 1: source

dsp_parser

Stage 2: where

| where EventID == 9212

Stage 3: where

| where SecurityIndicatorName == "Kerberos krbtgt account with old password"

Stage 4: extend

| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

TI map Email entity to OfficeActivity

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

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

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 4a3f5ed7-8da5-4ce2-af6f-c9ada45060f2
name: TI map Email entity to OfficeActivity
description: |
  'Identifies a match in OfficeActivity table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - 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-.]+$';
  let OfficeEvents = materialize(
    OfficeActivity
    | where isnotempty(UserId)
    | where TimeGenerated >= ago(dt_lookBack)
    | where UserId matches regex emailregex
    | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);
  let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);
  ThreatIntelligenceIndicator
  | where isnotempty(EmailSenderAddress)
  | where TimeGenerated >= ago(ioc_lookBack)
  | where tolower(EmailSenderAddress) in (OfficeActivityUPNs)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  // 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 (OfficeEvents) on $left.EmailSenderAddress == $right.UserId
  | where OfficeActivity_TimeGenerated < ExpirationDateTime
  | summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, UserId
  | project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, UserId, ClientIP, Operation, UserType, RecordType, OfficeWorkload, Parameters
  | extend Name = tostring(split(UserId, '@', 0)[0]), UPNSuffix = tostring(split(UserId, '@', 1)[0])
  | extend timestamp = OfficeActivity_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.7
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-.]+$';

Let Binding: OfficeEvents used in Stage 8

let OfficeEvents = materialize(
  OfficeActivity
  | where isnotempty(UserId)
  | where TimeGenerated >= ago(dt_lookBack)
  | where UserId matches regex emailregex
  | project-rename  OfficeActivity_TimeGenerated = TimeGenerated);

Let Binding: OfficeActivityUPNs used in Stage 4

let OfficeActivityUPNs = OfficeEvents | distinct UserId = tolower(UserId) | summarize make_list(UserId);

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(EmailSenderAddress)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: where

| where tolower(EmailSenderAddress) in (OfficeActivityUPNs)

Stage 5: summarize

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

Stage 6: where

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

Stage 7: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 8: join

| join kind=innerunique (OfficeEvents) on $left.EmailSenderAddress == $right.UserId

Stage 9: where

where OfficeActivity_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize OfficeActivity_TimeGenerated by IndicatorId, UserId

Stage 11: project

project ActivityGroupNames, ClientIP, ConfidenceScore, Description, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, OfficeActivity_TimeGenerated, OfficeWorkload, Operation, Parameters, RecordType, ThreatType, Url, UserId, UserType

Stage 12: extend

extend Name, UPNSuffix

Stage 13: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
OfficeActivity_TimeGeneratedproject
OfficeWorkloadproject
Operationproject
Parametersproject
RecordTypeproject
ThreatTypeproject
Urlproject
UserIdproject
UserTypeproject
Nameextend
UPNSuffixextend
timestampextend

TI Map File Hash to Security Event

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

Identifies a match in Security Event data from any File Hash IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: 9f7dc779-1e51-4925-ae4a-db1db933077f
name: TI Map File Hash to Security Event
description: |
  'Identifies a match in Security Event data from any File Hash IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - 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;
  let ioc_lookBack = 14d;
  ThreatIntelIndicators
  //extract key part of kv pair
        | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
        | where IndicatorType == "file"
        | extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))
        | extend FileHashValue = toupper(ObservableValue)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend FileHashValue = toupper(FileHashValue)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | project-reorder *, FileHashType, FileHashValue, Type
  // 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 ( union isfuzzy=true
    (SecurityEvent | where TimeGenerated >= ago(dt_lookBack)
        | where EventID in ("8003","8002","8005")
        | extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(FileHash)
    ),
    (WindowsEvent | where TimeGenerated >= ago(dt_lookBack)
        | where EventID in ("8003","8002","8005")
        | extend SecurityEvent_TimeGenerated = TimeGenerated, Event = EventID, FileHash = toupper(EventData.FileHash)
    )
  )
  on $left.FileHashValue == $right.FileHash
  | where SecurityEvent_TimeGenerated < ValidUntil
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, FileHash
  | project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ValidUntil, Confidence,
  Process, FileHash, Computer, Account, Event, FileHashValue, FileHashType, Url
  | extend NTDomain = tostring(split(Account, '\\', 0)[0]), Name = tostring(split(Account, '\\', 1)[0])
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')) 
  | extend timestamp = SecurityEvent_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: FileHashValue
      - identifier: Algorithm
        columnName: FileHashType
version: 1.4.10
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType == "file"

Stage 4: extend

| extend FileHashType = replace("'", "", substring(ObservableKey, indexof(ObservableKey, "hashes.") + 7, strlen(ObservableKey) - indexof(ObservableKey, "hashes.") - 7))

Stage 5: extend

| extend FileHashValue = toupper(ObservableValue)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend (3 consecutive steps)

| extend FileHashValue = toupper(FileHashValue)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

Stage 8: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 9: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 10: project-reorder

| project-reorder *, FileHashType, FileHashValue, Type

Stage 11: join

join kind=innerunique (...) on $left.FileHashValue, $right.FileHash

Stage 12: where

where SecurityEvent_TimeGenerated < ValidUntil

Stage 13: extend

extend Description

Stage 14: extend

extend ActivityGroupNames

Stage 15: summarize

summarize SecurityEvent_TimeGenerated by IndicatorId, FileHash

Stage 16: project

project Account, ActivityGroupNames, Computer, Confidence, Description, Event, FileHash, FileHashType, FileHashValue, IndicatorId, Process, SecurityEvent_TimeGenerated, Url, ValidUntil

Stage 17: extend (3 consecutive steps)

extend DnsDomain, HostName, NTDomain, Name, timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityGroupNamesproject
Computerproject
Confidenceproject
Descriptionproject
Eventproject
FileHashproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
Processproject
SecurityEvent_TimeGeneratedproject
Urlproject
ValidUntilproject
NTDomainextend
Nameextend
DnsDomainextend
HostNameextend
timestampextend

TIE user accounts issues

#
Severity
low
Time window
2h
Group by
Severity
Source
github.com/Azure/Azure-Sentinel

'Searches for triggered Indicators of Exposures related to user accounts issues.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: c4562ef3-d821-4089-b6c0-120d95c855e6
name: TIE user accounts issues
description: |
  'Searches for triggered Indicators of Exposures related to user accounts issues.'
severity: Low
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

User Assigned New Privileged Role

#
Status
available
Severity
high
Time window
1h
Group by
InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, OperationName, Result, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

Identifies when a new eligible or active privileged role is assigned to a user. Does not alert on PIM activations. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 746ddb63-f51b-4563-b449-a8b13cf302ec
name: User Assigned New Privileged Role
description: |
  'Identifies when a new eligible or active privileged role is assigned to a user. Does not alert on PIM activations. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Persistence
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  AuditLogs
  | where Category =~ "RoleManagement"
  | where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
  | mv-apply TargetResourceSubject = TargetResources on 
    (
        where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
        | extend Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
                 subjectProps = TargetResourceSubject.modifiedProperties
    )
  | mv-apply TargetResourceRole = TargetResources on 
    (
      // mimic modifiedProperties so we can use the same logic to get the role name regardless of where it comes from
      where TargetResourceRole.type in~ ("Role")
      | extend roleProps = pack_array(bag_pack("displayName","Role.DisplayName", "newValue", TargetResourceRole.displayName))
    )
  | mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) on 
    ( 
      where Property.displayName =~ "Role.DisplayName"
        | extend RoleName = trim('"',tostring(Property.newValue))
    )
  | where RoleName contains "Admin"
  | 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 Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
  // Comment below to alert for PIM activations
  | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
  | summarize by bin(TimeGenerated, 1h), OperationName, RoleName, Target, Initiator, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result
  | extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0]), InitiatorName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatorName
      - identifier: UPNSuffix
        columnName: InitiatorUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "RoleManagement"

Stage 3: where

| where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")

Stage 4: where

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

Stage 5: kusto:mv-apply

| mv-apply TargetResourceSubject = TargetResources on 
  (
      where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
      | extend Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
               subjectProps = TargetResourceSubject.modifiedProperties
  )

Stage 6: kusto:mv-apply

| mv-apply TargetResourceRole = TargetResources on 
  (
    where TargetResourceRole.type in~ ("Role")
    | extend roleProps = pack_array(bag_pack("displayName","Role.DisplayName", "newValue", TargetResourceRole.displayName))
  )

Stage 7: kusto:mv-apply

| mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) on 
  ( 
    where Property.displayName =~ "Role.DisplayName"
      | extend RoleName = trim('"',tostring(Property.newValue))
  )

Stage 8: where

| where RoleName contains "Admin"

Stage 9: extend (6 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 Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)

Stage 10: where

| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"

Stage 11: summarize

| summarize by bin(TimeGenerated, 1h), OperationName, RoleName, Target, Initiator, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result

Stage 12: extend

| extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0]), InitiatorName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypein
  • Assign corpus 6 (kusto 6)
  • AssignEligibleRole corpus 5 (kusto 5)
  • CreateRequestGrantedRole corpus 2 (kusto 2)
  • CreateRequestPermanentEligibleRole corpus 2 (kusto 2)
  • CreateRequestPermanentGrantedRole corpus 2 (kusto 2)
field:"AADOperationType" kind:in
ActivityDisplayNamematch
  • Add eligible member to role transforms: term corpus 9 (kusto 9)
  • Add member to role transforms: term corpus 9 (kusto 9)
field:"azure_ad::activity_display_name" kind:match
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
Initiatorne
  • MS-PIM
  • MS-PIM-Fairfax
field:"Initiator" kind:ne
Property.displayNameeq
  • Role.DisplayName transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"Role.DisplayName"
RoleNamecontains
  • Admin
field:"RoleName" kind:contains value:"Admin"
TargetResourceRole.typein
  • Role transforms: kusto_dynamic_path
field:"TargetResourceRole.type" kind:in value:"Role"
TargetResourceSubject.typein
  • ServicePrincipal transforms: kusto_dynamic_path
  • User transforms: kusto_dynamic_path
field:"TargetResourceSubject.type" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAadUserIdsummarize
InitiatingAppNamesummarize
InitiatingAppServicePrincipalIdsummarize
InitiatingIpAddresssummarize
InitiatingUserPrincipalNamesummarize
Initiatorsummarize
OperationNamesummarize
Resultsummarize
RoleNamesummarize
Targetsummarize
InitiatorNameextend
InitiatorUPNSuffixextend
TargetNameextend
TargetUPNSuffixextend

Back to all Kusto rules