Detection rules › Kusto

Kusto rules: 0j

[Entra ID] Suspicious Continuous OAuth Token Usage

#
Status
available
Severity
high
Time window
1d
Group by
ResultType, UniqueTokenIdentifier
Source
github.com/Azure/Azure-Sentinel

Detects repeated use of the same OAuth token across different IPs or locations over time. This can indicate token theft or session abuse.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 67802748-435b-4f80-9f61-b9a9ac6ea15c
name: "[Entra ID] Suspicious Continuous OAuth Token Usage"
version: 1.0.0
kind: Scheduled
description: |
  Detects repeated use of the same OAuth token across different IPs or locations over time. This can indicate token theft or session abuse.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
      - SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1606
query: |
  // =====================
  // Low-noise Token Reuse Detector (fixed timespan calc)
  // =====================
  let GapDays = 1;
  let MinIPChanges = 2;
  let MinLocChanges = 2;
  let ExcludeApps = dynamic([
      "Microsoft Authentication Broker",
      "Microsoft Teams",
      "Office 365"
      ]);
  let Src =
      union isfuzzy=true
          AADNonInteractiveUserSignInLogs,
          SigninLogs
      | where ResultType == 0
      | where isnotempty(UniqueTokenIdentifier);
  let Past =
      Src
      | where TimeGenerated between (ago(14d) .. ago(1d))
      | summarize
          PastLastSeen = max(TimeGenerated),
          PastUPNs     = make_set(UserPrincipalName, 20),
          PastIPs      = make_set(IPAddress, 50),
          PastLocs     = make_set(tostring(Location), 50),
          PastApps     = make_set(AppDisplayName, 50)
          by UniqueTokenIdentifier;
  let Last24h =
      Src
      | where TimeGenerated >= ago(1d)
      //| where AppDisplayName !in (ExcludeApps)
      | summarize
          CurrentFirstSeen = min(TimeGenerated),
          CurrentLastSeen  = max(TimeGenerated),
          CurrentUPNs      = make_set(UserPrincipalName, 20),
          CurrentIPs       = make_set(IPAddress, 50),
          CurrentLocs      = make_set(tostring(Location), 50),
          CurrentApps      = make_set(AppDisplayName, 50),
          IPCount          = dcount(IPAddress),
          LocCount         = dcount(tostring(Location))
          by UniqueTokenIdentifier, ResultType;
  Last24h
  | join kind=inner Past on UniqueTokenIdentifier
  | extend Gap = CurrentFirstSeen - PastLastSeen
  | extend GapThreshold = totimespan(strcat(GapDays, "d"))
  | where Gap >= GapThreshold
  | where IPCount >= MinIPChanges or LocCount >= MinLocChanges
  | extend NewIPs  = set_difference(CurrentIPs, PastIPs)
  | extend NewLocs = set_difference(CurrentLocs, PastLocs)
  | where array_length(NewIPs) > 0 or array_length(NewLocs) > 0
  | extend
      CurrentUPNCount = array_length(CurrentUPNs),
      PastUPNCount = array_length(PastUPNs)
  | project
      UniqueTokenIdentifier,
      PastLastSeen,
      CurrentFirstSeen,
      CurrentLastSeen,
      Gap,
      GapThreshold,
      CurrentUPNs,
      CurrentUPNCount,
      CurrentIPs,
      IPCount,
      NewIPs,
      CurrentLocs,
      LocCount,
      NewLocs,
      CurrentApps,
      PastApps,
      PastUPNs,
      ResultType
  | order by Gap desc, CurrentLastSeen desc
  | extend UserNames = strcat_array(CurrentUPNs, ",")
  | extend NewIP = strcat_array(NewIPs, ",")
  | extend FirstNewIP = tostring(NewIPs[0])
  | extend NewLoc = strcat_array(NewLocs, ",")
  | extend PastUPN = strcat_array(PastUPNs, ",")
  | extend Source_Network_IPLocation = ""
  | project
      Alert_Time_TW = datetime_utc_to_local(CurrentLastSeen, 'Asia/Taipei'),
      Alert_Time_UTC0 = CurrentLastSeen,
          Alert_Category_en = "Entra ID",
      Alert_SubCategory_en = "Anomaly Network Access User",
      Alert_Name_en = "Abnormal Sign-in Token Reuse",
      Alert_Description_en=strcat(
                           "At Taiwan time: ",
                           format_datetime(datetime_utc_to_local(CurrentLastSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
                           ", detected suspected Token Reuse behavior (UniqueTokenIdentifier appeared repeatedly and the interval reached the threshold).",
                           "Token:",
                           iff(isnotempty(UniqueTokenIdentifier), UniqueTokenIdentifier, "<NoTokenId>"),
                           ", users (last 2 days): ",
                           iff(isnotempty(tostring(UserNames)), tostring(UserNames), "<NoUserNames>"),
                           ", previous last seen: ",
                           format_datetime(datetime_utc_to_local(PastLastSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
                           ", current first seen: ",
                           format_datetime(datetime_utc_to_local(CurrentFirstSeen, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
                           ", gap: ",
                           tostring(Gap),
                           " days",
                           ", IP count: ",
                           tostring(IPCount),
                           ", current sign-in IP: ",
                           iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIP>"),
                           ", location count: ",
                           tostring(LocCount),
                           ", current sign-in location: ",
                           iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLoc>"),
                           "."
                       ),
      Alert_TriageStep_en=strcat(
                          " 1. Check whether this is truly token reuse. The reused IP is: ",
                          iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIPs>"),
                          ", current sign-in location: ",
                          iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLocs>"),
                          "  to determine whether it is an uncommon source, cross-country/cross-region, or anonymous cloud egress.",
                          " 2. Check whether multiple accounts share the same token. Current triggering account count: ",
                          tostring(CurrentUPNCount),
                          ", accounts that previously triggered this token: ",
                          tostring(PastUPN),
                          "; if the UPN count is greater than 1, prioritize suspicion of token leakage or proxy/automation abuse.",
                          " 3. Check the applications currently involved in sign-in: ",
                          iff(isnotempty(tostring(CurrentApps)), tostring(CurrentApps), "<NoCurrentApps>"),
                          "previous sign-in applications: ",
                          iff(isnotempty(tostring(PastApps)), tostring(PastApps), "<NoPastApps>"),
                          "  to determine whether they include administrative/highly sensitive applications or abnormally newly added apps."
                      ),
      Alert_Containment_en=strcat(
                           "1. Immediately revoke sign-in tokens/sessions and force re-sign-in for users (last 2 days): ",
                           iff(isnotempty(tostring(UserNames)), tostring(UserNames), "<NoUserNames>"),
                           "  to block continued access using the reused token.  ",
                           "2. If the current sign-in IP or location is abnormal, immediately block the current sign-in IP: ",
                           iff(isnotempty(tostring(NewIP)), tostring(NewIP), "<NoNewIP>"),
                           ", current sign-in location: ",
                           iff(isnotempty(tostring(NewLoc)), tostring(NewLoc), "<NoNewLoc>"),
                           ", and tighten Conditional Access (MFA/compliant device/named location).  ",
                           "3. Immediately require account security recovery: reset the password, re-register MFA, and check for suspicious devices or added authentication methods.  "
                       ),
      Alert_Remediation_en=strcat(
                           "1. Strengthen risk-based access: establish Conditional Access policies to block new IPs/new locations.  ",
                           "2. Implement token protection and endpoint governance: promote compliant devices, reduce long-lived token risk, and restrict access from unmanaged devices or legacy clients.  ",
                           "3. Inventory automation and applications: regularly check for abnormally added applications and high-privilege applications, and remove unnecessary permissions and old credentials.  ",
                           "4. Establish automated response: automatically revoke tokens, block IPs, notify users for confirmation, and create incident tickets for follow-up investigation when alerts trigger."
                       ),
              Event_Code = ResultType,
      //Event_Description = ResultDescription,
      Event_TimeRange_Start_UTC0 = CurrentFirstSeen,
      Event_TimeRange_End_UTC0 = CurrentLastSeen,
      Event_TimeRange_Start_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),
      Event_TimeRange_End_TW = datetime_utc_to_local(CurrentFirstSeen, 'Asia/Taipei'),
      Source_Identity_FullName = UserNames,
      Source_Network_IPAddress = NewIP,
      Source_Network_IPLocation = Source_Network_IPLocation,
      Target_Identity_FullName = UniqueTokenIdentifier,
      //Target_Network_IPAddress = UniqueTokenIdentifier,
      //Target_Resource_ID = "",
      Target_Resource_Name = "Microsoft Entra ID",
      Target_Resource_Type = "Microsoft Entra ID"
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Source_Identity_FullName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Source_Network_IPAddress
alertDetailsOverride:
  alertDynamicProperties: []

Stages and Predicates

Parameters

let GapDays = 1;
let MinIPChanges = 2;
let MinLocChanges = 2;

let Src and let Last24h are inlined into the numbered stages below.

Let Binding: ExcludeApps

let ExcludeApps = dynamic([
    "Microsoft Authentication Broker",
    "Microsoft Teams",
    "Office 365"
    ]);

Let Binding: Past used in Stage 8

let Past = Src
    | where TimeGenerated between (ago(14d) .. ago(1d))
    | summarize
        PastLastSeen = max(TimeGenerated),
        PastUPNs     = make_set(UserPrincipalName, 20),
        PastIPs      = make_set(IPAddress, 50),
        PastLocs     = make_set(tostring(Location), 50),
        PastApps     = make_set(AppDisplayName, 50)
        by UniqueTokenIdentifier;

Stages 1 to 7 define let Last24h as the rule's main pipeline source. Stages 8 to 20 use it.

Stage 1: union

union of 2 branches

Stage 2: source

AADNonInteractiveUserSignInLogs

Stage 3: source

SigninLogs

Stage 4: where

where ResultType == 0

Stage 5: where

where isnotempty(UniqueTokenIdentifier)

Stage 6: where

where TimeGenerated >= ago(86400s)

Stage 7: summarize

summarize CurrentApps, CurrentFirstSeen, CurrentIPs, CurrentLastSeen, CurrentLocs, CurrentUPNs, IPCount, LocCount by UniqueTokenIdentifier, ResultType

Stage 8: join

join kind=inner (Past) on UniqueTokenIdentifier

Stage 9: extend

extend Gap

Stage 10: extend

extend GapThreshold

Stage 11: where

where Gap >= GapThreshold

Stage 12: where

where (IPCount >= 2 or LocCount >= 2)

Stage 13: extend

extend NewIPs

Stage 14: extend

extend NewLocs

Stage 15: where

where (NewIPs > 0 or NewLocs > 0)

Stage 16: extend

extend CurrentUPNCount, PastUPNCount

Stage 17: project

project CurrentApps, CurrentFirstSeen, CurrentIPs, CurrentLastSeen, CurrentLocs, CurrentUPNCount, CurrentUPNs, Gap, GapThreshold, IPCount, LocCount, NewIPs, NewLocs, PastApps, PastLastSeen, PastUPNs, ResultType, UniqueTokenIdentifier

Stage 18: sort

sort by CurrentLastSeen, Gap

Stage 19: extend (6 consecutive steps)

extend FirstNewIP, NewIP, NewLoc, PastUPN, Source_Network_IPLocation, UserNames

Stage 20: project

project Alert_Category_en, Alert_Containment_en, Alert_Description_en, Alert_Name_en, Alert_Remediation_en, Alert_SubCategory_en, Alert_Time_TW, Alert_Time_UTC0, Alert_TriageStep_en, Event_Code, Event_TimeRange_End_TW, Event_TimeRange_End_UTC0, Event_TimeRange_Start_TW, Event_TimeRange_Start_UTC0, Source_Identity_FullName, Source_Network_IPAddress, Source_Network_IPLocation, Target_Identity_FullName, Target_Resource_Name, Target_Resource_Type

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Alert_Category_enproject
Alert_Containment_enproject
Alert_Description_enproject
Alert_Name_enproject
Alert_Remediation_enproject
Alert_SubCategory_enproject
Alert_Time_TWproject
Alert_Time_UTC0project
Alert_TriageStep_enproject
Event_Codeproject
Event_TimeRange_End_TWproject
Event_TimeRange_End_UTC0project
Event_TimeRange_Start_TWproject
Event_TimeRange_Start_UTC0project
Source_Identity_FullNameproject
Source_Network_IPAddressproject
Source_Network_IPLocationproject
Target_Identity_FullNameproject
Target_Resource_Nameproject
Target_Resource_Typeproject

AWS Security Hub - Detect CloudTrail trails lacking KMS encryption

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

This query detects AWS CloudTrail trails that are not configured to use server-side encryption with a customer managed KMS key using AWS Security Hub control CloudTrail.2 findings. Unencrypted CloudTrail logs increase the risk of unauthorized access to sensitive audit data at rest.

MITRE ATT&CK Coverage

Rule Body

id: 9c2f6c3b-7fd8-4c5a-9d9d-3c4f9e6a7b21
name: AWS Security Hub - Detect CloudTrail trails lacking KMS encryption
description: |
  This query detects AWS CloudTrail trails that are not configured to use server-side encryption with a customer managed KMS key using AWS Security Hub control CloudTrail.2 findings. 
  Unencrypted CloudTrail logs increase the risk of unauthorized access to sensitive audit data at rest.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWSSecurityHub
    dataTypes:
      - AWSSecurityHubFindings
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - DefenseEvasion
relevantTechniques:
  - T1565.001
  - T1562.008
tags:
  - CIS AWS Foundations Benchmark v1.4.0
  - CIS AWS Foundations Benchmark v1.2.0
  - NIST 800-53 r5
  - PCI DSS v3.2.1
query: |
  AWSSecurityHubFindings
  | where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
  | where tostring(AwsSecurityFindingGeneratorId) == "security-control/CloudTrail.2"
    or tostring(ComplianceSecurityControlId) == "CloudTrail.2"
  | mv-expand Resource = Resources
  | where tostring(Resource.Type) == "AwsCloudTrailTrail"
  | extend TrailId = tostring(Resource.Id)
  | summarize TimeGenerated = max(TimeGenerated)
      by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
         AwsSecurityFindingId, ComplianceSecurityControlId, TrailId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AwsAccountId
      - identifier: CloudAppAccountId
        columnName: AwsAccountId
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: TrailId
customDetails:
  ComplianceControlId: ComplianceSecurityControlId
  Region: AwsRegion
  FindingId: AwsSecurityFindingId
alertDetailsOverride:
  alertDisplayNameFormat: "AWS CloudTrail trail {{TrailId}} lacks KMS encryption"
  alertDescriptionFormat: "AWS CloudTrail trail ({{TrailId}}) lacks customer-managed KMS encryption for Account {{AwsAccountId}}."
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSSecurityHubFindings

Stage 2: where

| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"

Stage 3: where

| where tostring(AwsSecurityFindingGeneratorId) == "security-control/CloudTrail.2"
  or tostring(ComplianceSecurityControlId) == "CloudTrail.2"

Stage 4: mv-expand

| mv-expand Resource = Resources

Stage 5: where

| where tostring(Resource.Type) == "AwsCloudTrailTrail"

Stage 6: extend

| extend TrailId = tostring(Resource.Id)

Stage 7: summarize

| summarize TimeGenerated = max(TimeGenerated)
    by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
       AwsSecurityFindingId, ComplianceSecurityControlId, TrailId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AwsAccountIdsummarize
AwsRegionsummarize
AwsSecurityFindingDescriptionsummarize
AwsSecurityFindingIdsummarize
AwsSecurityFindingTitlesummarize
ComplianceSecurityControlIdsummarize
TimeGeneratedsummarize
TrailIdsummarize

AWSCloudTrail - Failed Attempts to Change AWS CloudTrail Logs

#
Status
available
Severity
low
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, EventName, EventSource, EventTypeName, RecipientAccountId, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName
Source
github.com/Azure/Azure-Sentinel

Identifies FAILED or denied attempts to manipulate AWS CloudTrail, CloudWatch/EventBridge, or VPC Flow Logs. Successful manipulation is covered by the higher-severity 'AWSCloudTrail - Successful Tampering with AWS CloudTrail Logs' rule. These failed attempts can indicate reconnaissance or an actor probing for insufficient permissions as part of defense evasion. For more information, visit: AWS CloudTrail API: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html AWS CloudWatch/EventBridge API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_Operations.html AWS DeleteFlowLogs API: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteFlowLogs.html

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 610d3850-c26f-4f20-8d86-f10fdf2425f5
name: AWSCloudTrail - Failed Attempts to Change AWS CloudTrail Logs
description: |
  Identifies FAILED or denied attempts to manipulate AWS CloudTrail, CloudWatch/EventBridge, or VPC Flow Logs.
  Successful manipulation is covered by the higher-severity 'AWSCloudTrail - Successful Tampering with AWS CloudTrail Logs' rule.
  These failed attempts can indicate reconnaissance or an actor probing for insufficient permissions as part of defense evasion.
  For more information, visit: 
  
  AWS CloudTrail API: https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_Operations.html 
  
  AWS CloudWatch/EventBridge API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_Operations.html 
  
  AWS DeleteFlowLogs API: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteFlowLogs.html
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.008
query: |
  let EventNameList = dynamic(["UpdateTrail","DeleteTrail","StopLogging","DeleteFlowLogs","DeleteEventBus","DeleteLogGroup"]);
  AWSCloudTrail
  | where (EventName in~ (EventNameList) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).enableLogFileValidation) == false) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).isMultiRegionTrail) == false)) and (isnotempty(ErrorMessage) or isnotempty(ErrorCode))
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
  UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
alertDetailsOverride:
  alertDisplayNameFormat: "AWS CloudTrail log manipulation FAILED attempt: {{EventName}} by {{AccountName}} from {{SourceIpAddress}}"
  alertDescriptionFormat: "Failed event {{EventName}} occurred in {{AWSRegion}} for account {{RecipientAccountId}}"
customDetails:
  EventName: EventName
  EventTypeName: EventTypeName
  EventSource: EventSource
  AWSRegion: AWSRegion
  UserAgent: UserAgent
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let EventNameList = dynamic(["UpdateTrail","DeleteTrail","StopLogging","DeleteFlowLogs","DeleteEventBus","DeleteLogGroup"]);

Stage 1: source

AWSCloudTrail

Stage 2: where

| where (EventName in~ (EventNameList) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).enableLogFileValidation) == false) or (EventName == "UpdateTrail" and (parse_json(RequestParameters).isMultiRegionTrail) == false)) and (isnotempty(ErrorMessage) or isnotempty(ErrorCode))

Stage 3: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 4: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion, EventSource

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ErrorCodeis_not_null
  • (no value, null check)
field:"ErrorCode" kind:is_not_null
ErrorMessageis_not_null
  • (no value, null check)
field:"ErrorMessage" kind:is_not_null
EventNameeq
  • UpdateTrail
field:"EventName" kind:eq value:"UpdateTrail"
EventNamein
  • DeleteEventBus
  • DeleteFlowLogs
  • DeleteLogGroup
  • DeleteTrail
  • StopLogging
  • UpdateTrail
field:"EventName" kind:in
RequestParameters.enableLogFileValidationeq
  • false transforms: kusto_accessor_expr:{"expression":"parse_json(RequestParameters).enableLogFileValidation","field":"RequestParameters.enableLogFileValidation"}, kusto_dynamic_path
field:"RequestParameters.enableLogFileValidation" kind:eq value:"false"
RequestParameters.isMultiRegionTraileq
  • false transforms: kusto_accessor_expr:{"expression":"parse_json(RequestParameters).isMultiRegionTrail","field":"RequestParameters.isMultiRegionTrail"}, kusto_dynamic_path
field:"RequestParameters.isMultiRegionTrail" kind:eq value:"false"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
EndTimeUtcsummarize
EventNamesummarize
EventSourcesummarize
EventTypeNamesummarize
RecipientAccountIdsummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimeUtcsummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityUserNamesummarize

Azure Machine Learning Write Operations

#
Status
available
Severity
low
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'Shows the most prevalent users who perform write operations on Azure Machine Learning resources. List the common source IP address for each of those accounts. If an operation is not from those IP addresses, it may be worthy of investigation.'

MITRE ATT&CK Coverage

Rule Body

id: 68c89998-8052-4c80-a1f6-9d81060b6d57
name: Azure Machine Learning Write Operations
description: |
  'Shows the most prevalent users who perform write operations on Azure Machine Learning resources. List the common source IP address for each of those accounts. If an operation is not from those IP addresses, it may be worthy of investigation.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
tactics:
  - InitialAccess
  - Execution
  - Impact
relevantTechniques:
  - T1078
  - T1059
  - T1496
queryFrequency: 1d  # Added queryFrequency property
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
query: |
  AzureActivity
  | where ResourceProviderValue == "MICROSOFT.MACHINELEARNINGSERVICES"  // Filter activities related to Microsoft Machine Learning Services
  | extend SCOPE = tostring(parse_json(Authorization).scope)  // Parse Authorization scope as string
  | extend subname = split(Hierarchy, "/")  // Split Hierarchy to extract Subscription Name and ID
  | extend ['Subscription Name'] = subname[-2], ['Subscription ID'] = subname[-1]  // Extract Subscription Name and ID
  | extend Properties = parse_json(Properties)  // Parse Properties as JSON
  | extend Properties_entity = tostring(Properties.entity)  // Cast Properties.entity to string
  | where isnotempty(Properties_entity)  // Filter activities where Properties.entity is not empty
  // | where Properties_entity contains "deepseek"  // Filter activities where Properties.entity contains "deepseek"
  | where OperationNameValue contains "write"  // Filter activities where OperationNameValue contains "write"
  | where OperationNameValue !contains "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"  // Exclude role assignments
  | extend LLM = tostring(split(Properties_entity, "/")[-1])  // Extract the last segment of Properties_entity and cast it to string
  | distinct TimeGenerated, tostring(['Subscription Name']), ResourceGroup, tostring(['Subscription ID']), Caller, CallerIpAddress, OperationNameValue, LLM, _ResourceId  // Select distinct relevant fields for output
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Caller
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: _ResourceId  
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

AzureActivity

Stage 2: where

| where ResourceProviderValue == "MICROSOFT.MACHINELEARNINGSERVICES"

Stage 3: extend (5 consecutive steps)

| extend SCOPE = tostring(parse_json(Authorization).scope)
| extend subname = split(Hierarchy, "/")
| extend ['Subscription Name'] = subname[-2], ['Subscription ID'] = subname[-1]
| extend Properties = parse_json(Properties)
| extend Properties_entity = tostring(Properties.entity)

Stage 4: where

| where isnotempty(Properties_entity)

Stage 5: where

| where OperationNameValue contains "write"

Stage 6: where

| where OperationNameValue !contains "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"

Stage 7: extend

| extend LLM = tostring(split(Properties_entity, "/")[-1])

Stage 8: distinct

| distinct TimeGenerated, tostring(['Subscription Name']), ResourceGroup, tostring(['Subscription ID']), Caller, CallerIpAddress, OperationNameValue, LLM, _ResourceId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
OperationNameValuecontainsMICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITEexcludes:OperationNameValue field:"OperationNameValue" value:"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SCOPEextend
subnameextend
Subscription IDextend
Subscription Nameextend
Propertiesextend
Properties_entityextend
LLMextend

Bitglass - Multiple failed logins

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

'Detects multiple failed logins.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 7c570bfc-9f20-490e-80e8-b898c7ce4bda
name: Bitglass - Multiple failed logins
description: |
  'Detects multiple failed logins.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Bitglass
    dataTypes:
      - Bitglass
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let threshold = 10;
  Bitglass
  | where EventType =~ 'access'
  | where EventResultDetails =~ 'Failed login attempt.'
  | summarize count() by User, bin(TimeGenerated, 10m)
  | where count_ >= threshold
  | extend AccountCustomEntity = User
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

Bitglass

Stage 2: where

| where EventType =~ 'access'

Stage 3: where

| where EventResultDetails =~ 'Failed login attempt.'

Stage 4: summarize

| summarize count() by User, bin(TimeGenerated, 10m)

Stage 5: where

| where count_ >= threshold

Stage 6: extend

| extend AccountCustomEntity = User

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Usersummarize
AccountCustomEntityextend

Disabling Security Services via Registry

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

This query checks for processes modifying the registry to disable security features. This is a common technique used by threat actors for defence evasion.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 32b29155-3fd3-4a9e-a0ca-a67e2593b60b
name: Disabling Security Services via Registry
description: |
  This query checks for processes modifying the registry to disable security features. This is a common technique used by threat actors for defence evasion.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |   
  DeviceProcessEvents
  | where InitiatingProcessCommandLine has_all(@'"reg"', 'add', @'"HKLM\SOFTWARE\Policies\', '/v','/t', 'REG_DWORD', '/d', '/f')
    and InitiatingProcessCommandLine has_any('DisableRealtimeMonitoring', 'UseTPMKey', 'UseTPMKeyPIN', 'UseAdvancedStartup', 
    'EnableBDEWithNoTPM', 'RecoveryKeyMessageSource')
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:  
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUpn
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ProcessId
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where InitiatingProcessCommandLine has_all(@'"reg"', 'add', @'"HKLM\SOFTWARE\Policies\', '/v','/t', 'REG_DWORD', '/d', '/f')
  and InitiatingProcessCommandLine has_any('DisableRealtimeMonitoring', 'UseTPMKey', 'UseTPMKeyPIN', 'UseAdvancedStartup', 
  'EnableBDEWithNoTPM', 'RecoveryKeyMessageSource')

Stage 3: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 4: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InitiatingProcessCommandLinematch
  • "HKLM\SOFTWARE\Policies\ transforms: term corpus 2 (kusto 2)
  • "reg" transforms: term corpus 2 (kusto 2)
  • /d transforms: term corpus 2 (kusto 2)
  • /f transforms: term corpus 2 (kusto 2)
  • /t transforms: term corpus 2 (kusto 2)
  • /v transforms: term corpus 2 (kusto 2)
  • DisableRealtimeMonitoring transforms: term corpus 2 (kusto 2)
  • EnableBDEWithNoTPM transforms: term corpus 2 (kusto 2)
  • REG_DWORD transforms: term corpus 2 (kusto 2)
  • RecoveryKeyMessageSource transforms: term corpus 2 (kusto 2)
  • UseAdvancedStartup transforms: term corpus 2 (kusto 2)
  • UseTPMKey transforms: term corpus 2 (kusto 2)
  • UseTPMKeyPIN transforms: term corpus 2 (kusto 2)
  • add transforms: term corpus 3 (kusto 3)
field:"ParentCommandLine" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostNameextend
DnsDomainextend

OracleDBAudit - Connection to database from unknown IP

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

'Detects when user connects to a database from IP address which is not present in AllowList.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 80b1dd6d-1aea-471e-be7a-a4a0afdeec80
name: OracleDBAudit - Connection to database from unknown IP
description: |
  'Detects when user connects to a database from IP address which is not present in AllowList.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  let ip_allowlist = dynamic(['127.0.0.2']);
  OracleDatabaseAuditEvent
  | where isnotempty(SrcIpAddr)
  | where DbAction =~ 'CONNECT'
  | where SrcIpAddr !in (ip_allowlist)
  | project SrcIpAddr, DstUserName
  | extend AccountCustomEntity = DstUserName
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let ip_allowlist = dynamic(['127.0.0.2']);

Stage 1: source

OracleDatabaseAuditEvent

Stage 2: where

| where isnotempty(SrcIpAddr)

Stage 3: where

| where DbAction =~ 'CONNECT'

Stage 4: where

| where SrcIpAddr !in (ip_allowlist)

Stage 5: project

| project SrcIpAddr, DstUserName

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Stage 7: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DbActioneq
  • CONNECT
field:"DbAction" kind:eq value:"CONNECT"
SrcIpAddris_not_null
  • (no value, null check)
field:"src_ip" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddreqip_allowlistexcludes:SrcIpAddr field:"SrcIpAddr" value:"ip_allowlist"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNameproject
SrcIpAddrproject
AccountCustomEntityextend
IPCustomEntityextend

PaloAlto - MAC address conflict

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

'Detects several users with the same MAC address.'

MITRE ATT&CK Coverage

Rule Body

id: 976d2eee-51cb-11ec-bf63-0242ac130002
name: PaloAlto - MAC address conflict
description: |
  'Detects several users with the same MAC address.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 2;
  PaloAltoCDLEvent
  | where EventResourceId =~ 'TRAFFIC'
  | where isnotempty(DestinationMACAddress) and isnotempty(DstUsername)
  | summarize UserSet = make_set(DstUsername) by DestinationMACAddress
  | extend Users = array_length(UserSet)
  | where Users >= threshold
  | extend AccountCustomEntity = UserSet, IPCustomEntity = DestinationMACAddress
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 2;

Stage 1: source

PaloAltoCDLEvent

Stage 2: where

| where EventResourceId =~ 'TRAFFIC'

Stage 3: where

| where isnotempty(DestinationMACAddress) and isnotempty(DstUsername)

Stage 4: summarize

| summarize UserSet = make_set(DstUsername) by DestinationMACAddress

Stage 5: extend

| extend Users = array_length(UserSet)

Stage 6: where

| where Users >= threshold

Stage 7: extend

| extend AccountCustomEntity = UserSet, IPCustomEntity = DestinationMACAddress

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationMACAddresssummarize
UserSetsummarize
Usersextend
AccountCustomEntityextend
IPCustomEntityextend

Password spray attack against Microsoft Entra ID application

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

Identifies evidence of password spray activity against Microsoft Entra ID applications by looking for failures from multiple accounts from the same IP address within a time window. If the number of accounts breaches the threshold just once, all failures from the IP address within the time range are bought into the result. Details on whether there were successful authentications by the IP address within the time window are also included. This can be an indicator that an attack was successful. The default failure acccount threshold is 5, Default time window for failures is 20m and default look back window is 1 day Note: Due to the number of possible accounts involved in a password spray it is not possible to map identities to a custom entity. References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: 48607a29-a26a-4abf-8078-a06dbdd174a4
name: Password spray attack against Microsoft Entra ID application
description: |
  'Identifies evidence of password spray activity against Microsoft Entra ID applications by looking for failures from multiple accounts from the same IP address within a time window. If the number of accounts breaches the threshold just once, all failures from the IP address within the time range are bought into the result. Details on whether there were successful authentications by the IP address within the time window are also included.
  This can be an indicator that an attack was successful.
  The default failure acccount threshold is 5, Default time window for failures is 20m and default look back window is 1 day
  Note: Due to the number of possible accounts involved in a password spray it is not possible to map identities to a custom entity.
  References: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
    let timeRange = 1d;
    let lookBack = 7d;
    let authenticationWindow = 20m;
    let authenticationThreshold = 5;
    let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
    let failureCodes = dynamic([50053, 50126, 50055]); // invalid password, account is locked - too many sign ins, expired password
    let successCodes = dynamic([0, 50055, 50057, 50155, 50105, 50133, 50005, 50076, 50079, 50173, 50158, 50072, 50074, 53003, 53000, 53001, 50129]);
    // Lookup up resolved identities from last 7 days
    let aadFunc = (tableName:string){
    let identityLookup = table(tableName)
    | where TimeGenerated >= ago(lookBack)
    | where not(Identity matches regex isGUID)
    | where isnotempty(UserId)
    | summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName, Type;
    // collect window threshold breaches
    table(tableName)
    | where TimeGenerated > ago(timeRange)
    | where ResultType in(failureCodes)
    | summarize FailedPrincipalCount = dcount(UserPrincipalName) by bin(TimeGenerated, authenticationWindow), IPAddress, AppDisplayName, Type
    | where FailedPrincipalCount >= authenticationThreshold
    | summarize WindowThresholdBreaches = count() by IPAddress, Type
    | join kind= inner (
    // where we breached a threshold, join the details back on all failure data
    table(tableName)
    | where TimeGenerated > ago(timeRange)
    | where ResultType in(failureCodes)
    | extend LocationDetails = todynamic(LocationDetails)
    | extend FullLocation = strcat(LocationDetails.countryOrRegion,'|', LocationDetails.state, '|', LocationDetails.city)
    | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), make_set(ClientAppUsed,20), make_set(FullLocation,20), FailureCount = count() by IPAddress, AppDisplayName, UserPrincipalName, UserDisplayName, Identity, UserId, Type
    // lookup any unresolved identities
    | extend UnresolvedUserId = iff(Identity matches regex isGUID, UserId, "")
    | join kind= leftouter (
     identityLookup
    ) on $left.UnresolvedUserId==$right.UserId
    | extend UserDisplayName=iff(isempty(lu_UserDisplayName), UserDisplayName, lu_UserDisplayName)
    | extend UserPrincipalName=iff(isempty(lu_UserPrincipalName), UserPrincipalName, lu_UserPrincipalName)
    | summarize StartTime = min(StartTime), EndTime = max(EndTime), make_set(UserPrincipalName,20), make_set(UserDisplayName,20), make_set(set_ClientAppUsed,20), make_set(set_FullLocation,20), make_list(FailureCount,20) by IPAddress, AppDisplayName, Type
    | extend FailedPrincipalCount = array_length(set_UserPrincipalName)
    ) on IPAddress
    | project IPAddress, StartTime, EndTime, TargetedApplication=AppDisplayName, FailedPrincipalCount, UserPrincipalNames=set_UserPrincipalName, UserDisplayNames=set_UserDisplayName, ClientAppsUsed=set_set_ClientAppUsed, Locations=set_set_FullLocation, FailureCountByPrincipal=list_FailureCount, WindowThresholdBreaches, Type
    | join kind= inner (
    table(tableName) // get data on success vs. failure history for each IP
    | where TimeGenerated > ago(timeRange)
    | where ResultType in(successCodes) or ResultType in(failureCodes) // success or failure types
    | summarize GlobalSuccessPrincipalCount = dcountif(UserPrincipalName, (ResultType in (successCodes))), ResultTypeSuccesses = make_set_if(ResultType, (ResultType in (successCodes))), GlobalFailPrincipalCount = dcountif(UserPrincipalName, (ResultType in (failureCodes))), ResultTypeFailures = make_set_if(ResultType, (ResultType in (failureCodes))) by IPAddress, Type
    | where GlobalFailPrincipalCount > GlobalSuccessPrincipalCount // where the number of failed principals is greater than success - eliminates FPs from IPs who authenticate successfully alot and as a side effect have alot of failures
    ) on IPAddress
    | project-away IPAddress1
    | extend timestamp=StartTime
    };
    let aadSignin = aadFunc("SigninLogs");
    let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
    union isfuzzy=true aadSignin, aadNonInt
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.7
kind: Scheduled

Stages and Predicates

Parameters

let timeRange = 1d;
let lookBack = 7d;
let authenticationWindow = 20m;
let authenticationThreshold = 5;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let failureCodes = dynamic([50053, 50126, 50055]);
let successCodes = dynamic([0, 50055, 50057, 50155, 50105, 50133, 50005, 50076, 50079, 50173, 50158, 50072, 50074, 53003, 53000, 53001, 50129]);
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc used in Stages 1, 2, 3

let aadFunc = (tableName:string){
let identityLookup = table(tableName)
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| where isnotempty(UserId)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName, Type;

Stage 1: union

union of 2 branches

Stage 2: source

aadFunc(...)

Stage 3: source

aadFunc(...)

Qakbot Campaign Self Deletion

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

This query was originally published in the threat analytics report, Qakbot blight lingers, seeds ransomware Qakbot is malware that steals login credentials from banking and financial services. It has been deployed against small businesses as well as major corporations. Some outbreaks have involved targeted ransomware campaigns that use a similar set of techniques. Links to related queries are listed under See also. The following query detects if an instance of Qakbot has attempted to overwrite its original binary. Reference - https://www.microsoft.com/security/blog/2017/11/06/mitigating-and-eliminating-info-stealing-qakbot-and-emotet-in-corporate-networks/

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Telemetry Coverage

Rule Body

id: 47c02e21-3949-4e05-a28e-576cd75ff6f6
name: Qakbot Campaign Self Deletion
description: |
  This query was originally published in the threat analytics report, Qakbot blight lingers, seeds ransomware
  Qakbot is malware that steals login credentials from banking and financial services. It has been deployed against small businesses as well as major corporations. Some outbreaks have involved targeted ransomware campaigns that use a similar set of techniques. Links to related queries are listed under See also.
  The following query detects if an instance of Qakbot has attempted to overwrite its original binary.
  Reference - https://www.microsoft.com/security/blog/2017/11/06/mitigating-and-eliminating-info-stealing-qakbot-and-emotet-in-corporate-networks/
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1070
tags:
  - Qakbot
  - Ransomware
  - Self Deletion
  - Defense Evasion
query: |
  DeviceProcessEvents 
  | where FileName =~ "ping.exe"
  | where InitiatingProcessFileName =~ "cmd.exe"
  | where (InitiatingProcessCommandLine has "calc.exe") and (InitiatingProcessCommandLine has "-n 6") and (InitiatingProcessCommandLine has "127.0.0.1")
  | project TimeGenerated, ProcessCommandLine, InitiatingProcessCommandLine, InitiatingProcessParentFileName, DeviceId, DeviceName
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where FileName =~ "ping.exe"

Stage 3: where

| where InitiatingProcessFileName =~ "cmd.exe"

Stage 4: where

| where (InitiatingProcessCommandLine has "calc.exe") and (InitiatingProcessCommandLine has "-n 6") and (InitiatingProcessCommandLine has "127.0.0.1")

Stage 5: project

| project TimeGenerated, ProcessCommandLine, InitiatingProcessCommandLine, InitiatingProcessParentFileName, DeviceId, DeviceName

Stage 6: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 7: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNameeq
  • ping.exe corpus 12 (elastic 8, splunk 2, kusto 2)
field:"process_basename" kind:eq value:"ping.exe"
InitiatingProcessCommandLinematch
  • -n 6 transforms: term
  • 127.0.0.1 transforms: term
  • calc.exe transforms: term
field:"ParentCommandLine" kind:match
InitiatingProcessFileNameeq
  • cmd.exe corpus 37 (elastic 32, splunk 4, kusto 1)
field:"parent_process_basename" kind:eq value:"cmd.exe"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdproject
DeviceNameproject
InitiatingProcessCommandLineproject
InitiatingProcessParentFileNameproject
ProcessCommandLineproject
TimeGeneratedproject
HostNameextend
DnsDomainextend

Solorigate Defender Detections

#
Severity
high
Time window
1d
Group by
$left.DeviceName, $right.CompromisedEntity
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Surfaces any Defender Alert for Solorigate Events. In Microsoft Sentinel the SecurityAlerts table includes only the Device Name of the affected device, this query joins the DeviceInfo table to clearly connect other information such as Device group, ip, logged on users etc. This way, the Microsoft Sentinel user can have all the pertinent device info in one view for all the the Solarigate Defender alerts.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: e70fa6e0-796a-4e85-9420-98b17b0bb749
name: Solorigate Defender Detections
description: |
  'Surfaces any Defender Alert for Solorigate Events. In Microsoft Sentinel the SecurityAlerts table includes only the Device Name of the affected device, this query joins the DeviceInfo table to clearly connect other information such as
   Device group, ip, logged on users etc. This way, the Microsoft Sentinel user can have all the pertinent device info in one view for all the the Solarigate Defender alerts.'
severity: High
requiredDataConnectors:
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert (MDATP)
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceInfo
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1195
tags:
  - Solorigate
  - NOBELIUM
query: |
  DeviceInfo
  | extend DeviceName = tolower(DeviceName)
  | join (SecurityAlert
  | where ProviderName =~ "MDATP"
  | extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
  | where ThreatName has "Solorigate"
  ) on $left.DeviceName == $right.CompromisedEntity
  | project TimeGenerated, DisplayName, ThreatName, CompromisedEntity, PublicIP, MachineGroup, AlertSeverity, Description, LoggedOnUsers, DeviceId, TenantId
  | extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
  | project-away DomainIndex
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: CompromisedEntity
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: PublicIP
version: 1.0.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - 0-day Vulnerability" ]

Stages and Predicates

Stage 1: source

DeviceInfo

Stage 2: extend

| extend DeviceName = tolower(DeviceName)

Stage 3: join

| join (SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where ThreatName has "Solorigate"
) on $left.DeviceName == $right.CompromisedEntity

Stage 4: project

project AlertSeverity, CompromisedEntity, Description, DeviceId, DisplayName, LoggedOnUsers, MachineGroup, PublicIP, TenantId, ThreatName, TimeGenerated

Stage 5: extend

extend DomainIndex, HostName

Stage 6: extend

extend HostNameDomain
HostNameDomain =
ifDomainIndex != -1substring(CompromisedEntity, (DomainIndex + 1))
elseCompromisedEntity

Stage 7: project-away

project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertSeverityproject
CompromisedEntityproject
Descriptionproject
DeviceIdproject
DisplayNameproject
LoggedOnUsersproject
MachineGroupproject
PublicIPproject
TenantIdproject
ThreatNameproject
TimeGeneratedproject
HostNameextend
HostNameDomainextend

SpyCloud Enterprise Malware Detection

#

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
12h
Source
github.com/Azure/Azure-Sentinel

'This alert creates an incident when an malware record is detected in the SpyCloud watchlist data'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 7ba50f9e-2f94-462b-a54b-8642b8c041f5
name: SpyCloud Enterprise Malware Detection
description: |
  'This alert creates an incident when an malware record is detected in the SpyCloud watchlist data'
severity: High
requiredDataConnectors: []
status: Available
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
tactics:
  - CredentialAccess
relevantTechniques:
  - T1555
query: |
  SpyCloudBreachDataWatchlist_CL
  | where Severity_s == '25'
  | project TimeGenerated, Document_Id_g, Source_Id_s, SpyCloud_Publish_Date_t, Email_s, Domain_s, Password_s, Password_Plaintext_s, Username_s, Infected_Machine_Id_g, Infected_Path_s, Infected_Time_t, Target_Domain_s, Target_SubDomain_s, User_Hostname_s, User_OS_s, Target_URL_s,IP_Address_s
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 12h
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride: null
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Infected_Machine_Id_g
      - identifier: DnsDomain
        columnName: User_Hostname_s
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Email_s
      - identifier: Name
        columnName: Username_s
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Target_Domain_s
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Target_SubDomain_s
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP_Address_s
customDetails:
  Document_Id: Document_Id_g
  Password: Password_s
  Password_Plaintext: Password_Plaintext_s
  Infected_Path: Infected_Path_s
  Infected_Time: Infected_Time_t
  Domain: Domain_s
  Source_Id: Source_Id_s
  PublishDate: SpyCloud_Publish_Date_t
  User_Host_Name: User_Hostname_s
sentinelEntitiesMappings: null
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

SpyCloudBreachDataWatchlist_CL

Stage 2: where

| where Severity_s == '25'

Stage 3: project

| project TimeGenerated, Document_Id_g, Source_Id_s, SpyCloud_Publish_Date_t, Email_s, Domain_s, Password_s, Password_Plaintext_s, Username_s, Infected_Machine_Id_g, Infected_Path_s, Infected_Time_t, Target_Domain_s, Target_SubDomain_s, User_Hostname_s, User_OS_s, Target_URL_s,IP_Address_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Document_Id_gproject
Domain_sproject
Email_sproject
IP_Address_sproject
Infected_Machine_Id_gproject
Infected_Path_sproject
Infected_Time_tproject
Password_Plaintext_sproject
Password_sproject
Source_Id_sproject
SpyCloud_Publish_Date_tproject
Target_Domain_sproject
Target_SubDomain_sproject
Target_URL_sproject
TimeGeneratedproject
User_Hostname_sproject
User_OS_sproject
Username_sproject

TI map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)

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

Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: a4025a76-6490-4e6b-bb69-d02be4b03f07
name: TI map IP entity to AzureNetworkAnalytics_CL (NSG Flow Logs)
description: |
  Identifies a match in AzureNetworkAnalytics_CL (NSG Flow Logs) from any IP IOC from TI that was Allowed
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureNetworkAnalytics_CL logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and AzureNetworkAnalytics_CL logs for NSG Flow information
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureNetworkAnalytics_CL
        | where TimeGenerated >= ago(dt_lookBack)
        | extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
        | extend PIPs = split(PublicIPs_s, '|', 0)
        | extend PIP = tostring(PIPs[0])
    )
    on $left.TI_ipEntity == $right.PIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where AzureNetworkAnalytics_CL_TimeGenerated < ExpirationDateTime
    // Filter out NSG Flow logs that are not allowed (FlowStatus_s == "A")
    | where FlowStatus_s == "A"
    // Group the results by IndicatorId and PIP (Public IP), and keep the log entry with the latest timestamp
    | summarize AzureNetworkAnalytics_CL_TimeGenerated = arg_max(AzureNetworkAnalytics_CL_TimeGenerated, *) by IndicatorId, PIP
    // Select the desired output fields
    | project AzureNetworkAnalytics_CL_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, Computer, FlowDirection_s, FlowStatus_s, FlowType_s, SrcPublicIPs_s, DestPublicIPs_s, PublicIPs_s, L7Protocol_s, DestPort_d, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
    // Extract hostname and DNS domain from the Computer field
    | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
    // Rename the timestamp field
    | extend timestamp = AzureNetworkAnalytics_CL_TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: TI_ipEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.3
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

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

Stage 6: summarize

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

Stage 7: where

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

Stage 8: join

IP_Indicators
| join kind=innerunique (
      AzureNetworkAnalytics_CL
      | where TimeGenerated >= ago(dt_lookBack)
      | extend AzureNetworkAnalytics_CL_TimeGenerated = TimeGenerated
      | extend PIPs = split(PublicIPs_s, '|', 0)
      | extend PIP = tostring(PIPs[0])
  )
  on $left.TI_ipEntity == $right.PIP

Stage 9: where

where AzureNetworkAnalytics_CL_TimeGenerated < ExpirationDateTime

Stage 10: where

where FlowStatus_s == "A"

Stage 11: summarize

summarize AzureNetworkAnalytics_CL_TimeGenerated by IndicatorId, PIP

Stage 12: project

project ActivityGroupNames, AzureNetworkAnalytics_CL_TimeGenerated, Computer, ConfidenceScore, Description, DestPort_d, DestPublicIPs_s, EmailSourceIpAddress, ExpirationDateTime, FlowDirection_s, FlowStatus_s, FlowType_s, IndicatorId, L7Protocol_s, NetworkDestinationIP, NetworkIP, NetworkSourceIP, PublicIPs_s, SrcPublicIPs_s, TI_ipEntity, ThreatType, Type, Url

Stage 13: extend

extend DnsDomain, HostName

Stage 14: extend

extend 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
ActivityGroupNamesproject
AzureNetworkAnalytics_CL_TimeGeneratedproject
Computerproject
ConfidenceScoreproject
Descriptionproject
DestPort_dproject
DestPublicIPs_sproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
FlowDirection_sproject
FlowStatus_sproject
FlowType_sproject
IndicatorIdproject
L7Protocol_sproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
PublicIPs_sproject
SrcPublicIPs_sproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
DnsDomainextend
HostNameextend
timestampextend

TI Map URL entity to Cloud App Events

#
Severity
medium
Time window
14d
Group by
IndicatorId, ObservableValue, Url
Source
github.com/Azure/Azure-Sentinel

Identifies compromises and attacks and detect malicious activities in one's URL entity from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: 526df43b-f514-477c-af7a-c8d3586457fb
name: TI Map URL entity to Cloud App Events
description: |
   'Identifies compromises and attacks and detect malicious activities in one's URL entity from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - CloudAppEvents
  - 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 == "url"
  | extend Url = ObservableValue
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | join kind=innerunique (CloudAppEvents
  | where TimeGenerated >= ago(dt_lookBack)
  | extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1, tostring(ActivityObjects))
  | extend userPrincipalName = tostring(RawEventData.UserId)
  | extend TargetResourceDisplayName = tostring(ActivityObjects[0].displayName)
  | extend CloudApps_TimeGenerated = TimeGenerated) on Url
  | where CloudApps_TimeGenerated < ValidUntil
  | summarize CloudApps_TimeGenerated = argmax(CloudApps_TimeGenerated, *) by IndicatorId, Url
  | extend 
    Description = column_ifexists("max_CloudApps_TimeGenerated_Description", ""),
    ActivityGroupNames = column_ifexists("max_CloudApps_TimeGenerated_ActivityGroupNames", ""),
    ThreatType = column_ifexists("max_CloudApps_TimeGenerated_ThreatType", ""),
    ExpirationDateTime = column_ifexists("max_CloudApps_TimeGenerated_ExpirationDateTime", ""),
    ConfidenceScore = column_ifexists("max_CloudApps_TimeGenerated_ConfidenceScore", ""),
    IPAddress = column_ifexists("max_CloudApps_TimeGenerated_IPAddress", ""),
    AccountObjectId = column_ifexists("max_CloudApps_TimeGenerated_AccountObjectId", ""),
    AccountDisplayName = column_ifexists("max_CloudApps_TimeGenerated_AccountDisplayName", ""),
    ObjectName = column_ifexists("max_CloudApps_TimeGenerated_ObjectName", ""),
    Application = column_ifexists("max_CloudApps_TimeGenerated_Application", ""),
    ApplicationID = column_ifexists("max_CloudApps_TimeGenerated_ApplicationId", ""),
    userPrincipalName = column_ifexists("max_CloudApps_TimeGenerated_userPrincipalName", "")
  | project CloudApps_TimeGenerated, Description, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, IPAddress, userPrincipalName, AccountObjectId, AccountDisplayName, ObjectName, Application, ApplicationID
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: ObjectGuid
        columnName: AccountObjectId
      - identifier: FullName
        columnName: userPrincipalName
      - identifier: DisplayName
        columnName: AccountDisplayName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: Application
      - identifier: AppId
        columnName: ApplicationID
version: 1.0.7
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 == "url"

Stage 4: extend

| extend Url = ObservableValue

Stage 5: extend

| extend IndicatorId = tostring(split(Id, "--")[2])

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: summarize

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

Stage 8: where

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

Stage 9: join

| join kind=innerunique (CloudAppEvents
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1, tostring(ActivityObjects))
| extend userPrincipalName = tostring(RawEventData.UserId)
| extend TargetResourceDisplayName = tostring(ActivityObjects[0].displayName)
| extend CloudApps_TimeGenerated = TimeGenerated) on Url

Stage 10: where

where CloudApps_TimeGenerated < ValidUntil

Stage 11: summarize

summarize CloudApps_TimeGenerated by IndicatorId, Url

Stage 12: extend

extend AccountDisplayName, AccountObjectId, ActivityGroupNames, Application, ApplicationID, ConfidenceScore, Description, ExpirationDateTime, IPAddress, ObjectName, ThreatType, userPrincipalName

Stage 13: project

project AccountDisplayName, AccountObjectId, Application, ApplicationID, CloudApps_TimeGenerated, ConfidenceScore, Description, ExpirationDateTime, IPAddress, IndicatorId, ObjectName, ThreatType, Url, userPrincipalName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDisplayNameproject
AccountObjectIdproject
Applicationproject
ApplicationIDproject
CloudApps_TimeGeneratedproject
ConfidenceScoreproject
Descriptionproject
ExpirationDateTimeproject
IPAddressproject
IndicatorIdproject
ObjectNameproject
ThreatTypeproject
Urlproject
userPrincipalNameproject

Zscaler - Shared ZPA session

#
Status
available
Severity
high
Time window
1d
Group by
DstUserName, NetworkSessionId, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

'Detects shared ZPA session.'

MITRE ATT&CK Coverage

Rule Body

id: 40a98355-0e52-479f-8c91-4ab659cba878
name: Zscaler - Shared ZPA session
description: |
  'Detects shared ZPA session.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ZPA_CL
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
  - T1133
query: |
  let open_sessions =
  ZPAEvent
  | where DvcAction == 'open'
  | summarize timestampstart = min(TimeGenerated) by DstUserName, SrcIpAddr, NetworkSessionId
  | sort by timestampstart asc;
  let closed_sessions =
  ZPAEvent
  | where TimeGenerated > ago(1h)
  | where DvcAction == 'close'
  | summarize timestampend = max(TimeGenerated) by DstUserName, SrcIpAddr, NetworkSessionId
  | sort by timestampend asc;
  open_sessions
  | join (closed_sessions) on DstUserName
  | sort by DstUserName, timestampstart
  | extend prev_session_closetime = prev(timestampend,1)
  | extend prev_session_starttime = prev(timestampstart,1)
  | extend PreviousSrcIpAddr = prev(SrcIpAddr, 1)
  | extend prev_sessionuser = prev(DstUserName, 1) 
  | where DstUserName == prev_sessionuser
  | where SrcIpAddr != PreviousSrcIpAddr
  | where prev_session_closetime > timestampstart
  | project DstUserName, SrcIpAddr, PreviousSrcIpAddr
  | extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

let open_sessions is inlined into the numbered stages below.

Let Binding: closed_sessions used in Stage 5

let closed_sessions = ZPAEvent
| where TimeGenerated > ago(1h)
| where DvcAction == 'close'
| summarize timestampend = max(TimeGenerated) by DstUserName, SrcIpAddr, NetworkSessionId
| sort by timestampend asc;

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

Stage 1: source

ZPAEvent

Stage 2: where

| where DvcAction == 'open'

Stage 3: summarize

| summarize timestampstart = min(TimeGenerated) by DstUserName, SrcIpAddr, NetworkSessionId

Stage 4: sort

| sort by timestampstart asc

Stage 5: join

open_sessions
| join (closed_sessions) on DstUserName

Stage 6: sort

sort by DstUserName, timestampstart

Stage 7: extend (4 consecutive steps)

extend PreviousSrcIpAddr, prev_session_closetime, prev_session_starttime, prev_sessionuser

Stage 8: where

where DstUserName =~ prev_sessionuser

Stage 9: where

where SrcIpAddr != PreviousSrcIpAddr

Stage 10: where

where prev_session_closetime > timestampstart

Stage 11: project

project DstUserName, PreviousSrcIpAddr, SrcIpAddr

Stage 12: extend

extend AccountCustomEntity, IPCustomEntity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DvcActioneq
  • close
  • open
field:"DvcAction" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNameproject
PreviousSrcIpAddrproject
SrcIpAddrproject
AccountCustomEntityextend
IPCustomEntityextend

Back to all Kusto rules