Detection rules › Kusto

High-Risk Cross-Cloud User Impersonation

Severity
medium
Time window
1d
Group by
IPAddress, RiskEventTypes, RiskEventTypes_V2, SourceIpAddress, UserPrincipalName, signInTime
Source
github.com/Azure/Azure-Sentinel

'This detection focuses on identifying high-risk cross-cloud activities and sign-in anomalies that may indicate potential security threats. The query starts by analyzing Microsoft Entra ID Signin Logs to pinpoint instances where specific applications, risk levels, and result types align. It then correlates this information with relevant AWS CloudTrail events to identify activities across Azure and AWS environments.'

MITRE ATT&CK coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: f4a28082-2808-4783-9736-33c1ae117475
name: High-Risk Cross-Cloud User Impersonation 
description: |
    'This detection focuses on identifying high-risk cross-cloud activities and sign-in anomalies that may indicate potential security threats. The query starts by analyzing Microsoft Entra ID Signin Logs to pinpoint instances where specific applications, risk levels, and result types align. It then correlates this information with relevant AWS CloudTrail events to identify activities across Azure and AWS environments.'
severity: Medium
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1134
  - T1078.002
  - T1078.004
query: |
  // Retrieve Azure AD SigninLogs within the last day
  SigninLogs 
  // Filter for specific AppDisplayNames, ResultType, and Risk Levels
  | where AppDisplayName in ("Azure Portal", "ADFS Trust", "Microsoft Azure PowerShell")
      and RiskLevelAggregated == "high"
      and RiskLevelDuringSignIn == "high"
  // Summarize AppDisplayNames by relevant attributes
  | extend Result = iff(ResultType == 0, "Successful Signin", "Failed Signin")
  | summarize make_set(AppDisplayName)
      by
      IPAddress,
      signInTime=TimeGenerated,
      UserPrincipalName,
      RiskEventTypes,
      RiskEventTypes_V2
  // Inner join with AWS CloudTrail events
  | join kind=inner (
      AWSCloudTrail
      | where isempty(ErrorMessage)
      | where EventSource in ("iam.amazonaws.com", "identitystore.amazonaws.com", "workmail.amazonaws.com", "workdocs.amazonaws.com")
      // List of AWS event names
      | where EventName in~ ("CreateRole", "DeleteRole", "CreateUser", "CreateAccessKey", "DeleteAccessKey", "CreateGroup", "AddUserToGroup", "ChangePassword", "DeleteGroup", "DeleteUser", "RemoveUserFromGroup", "CreateVirtualMFADevice", "DeleteLoginProfile", "CreateOrganization", "SetDefaultMailDomain", "SetMailUserDetails", "CreateMailUser", "ResetPassword", "RegisterToWorkMail", "DisableMailUsers", "EnableMailUsers", "DeleteServiceSpecificCredential", "CreateServiceSpecificCredential", "UpdateAccountEmailAddress", "DeleteGroupPolicy", "UploadServerCertificate")  
      // Summarize relevant attributes
      | summarize make_set(RequestParameters), make_set(ResponseElements)
          by
          SourceIpAddress,
          UserIdentityArn,
          UserIdentityType,
          EventName,
          EventTime=TimeGenerated,
          EventSource
      )
      on $left.IPAddress == $right.SourceIpAddress  
  // Calculate time difference in hours between AWS event and Azure sign-in
  | extend timedef = datetime_diff("hour", EventTime, signInTime)
  // Filter for time differences within a certain range
  | where timedef between (0 .. 8)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
   AwsUser: UserIdentityArn
   RiskEventTypes: RiskEventTypes
   AzureUser: UserPrincipalName
   AWSEventName: EventName
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

SigninLogs

Stage 2: where

| where AppDisplayName in ("Azure Portal", "ADFS Trust", "Microsoft Azure PowerShell")
    and RiskLevelAggregated == "high"
    and RiskLevelDuringSignIn == "high"

Stage 3: extend

| extend Result = iff(ResultType == 0, "Successful Signin", "Failed Signin")
Result =
ifResultType == 0"Successful Signin"
else"Failed Signin"

Stage 4: summarize

| summarize make_set(AppDisplayName)
    by
    IPAddress,
    signInTime=TimeGenerated,
    UserPrincipalName,
    RiskEventTypes,
    RiskEventTypes_V2

Stage 5: join

| join kind=inner (
    AWSCloudTrail
    | where isempty(ErrorMessage)
    | where EventSource in ("iam.amazonaws.com", "identitystore.amazonaws.com", "workmail.amazonaws.com", "workdocs.amazonaws.com")
    | where EventName in~ ("CreateRole", "DeleteRole", "CreateUser", "CreateAccessKey", "DeleteAccessKey", "CreateGroup", "AddUserToGroup", "ChangePassword", "DeleteGroup", "DeleteUser", "RemoveUserFromGroup", "CreateVirtualMFADevice", "DeleteLoginProfile", "CreateOrganization", "SetDefaultMailDomain", "SetMailUserDetails", "CreateMailUser", "ResetPassword", "RegisterToWorkMail", "DisableMailUsers", "EnableMailUsers", "DeleteServiceSpecificCredential", "CreateServiceSpecificCredential", "UpdateAccountEmailAddress", "DeleteGroupPolicy", "UploadServerCertificate")  
    | summarize make_set(RequestParameters), make_set(ResponseElements)
        by
        SourceIpAddress,
        UserIdentityArn,
        UserIdentityType,
        EventName,
        EventTime=TimeGenerated,
        EventSource
    )
    on $left.IPAddress == $right.SourceIpAddress

Stage 6: extend

| extend timedef = datetime_diff("hour", EventTime, signInTime)

Stage 7: where

| where timedef between (0 .. 8)

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
AppDisplayNamein
  • ADFS Trust transforms: cased
  • Azure Portal transforms: cased
  • Microsoft Azure PowerShell transforms: cased
ErrorMessageis_null
  • (no value, null check)
EventNamein
  • AddUserToGroup
  • ChangePassword
  • CreateAccessKey
  • CreateGroup
  • CreateMailUser
  • CreateOrganization
  • CreateRole
  • CreateServiceSpecificCredential
  • CreateUser
  • CreateVirtualMFADevice
  • DeleteAccessKey
  • DeleteGroup
  • DeleteGroupPolicy
  • DeleteLoginProfile
  • DeleteRole
  • DeleteServiceSpecificCredential
  • DeleteUser
  • DisableMailUsers
  • EnableMailUsers
  • RegisterToWorkMail
  • RemoveUserFromGroup
  • ResetPassword
  • SetDefaultMailDomain
  • SetMailUserDetails
  • UpdateAccountEmailAddress
  • UploadServerCertificate
EventSourcein
  • iam.amazonaws.com transforms: cased
  • identitystore.amazonaws.com transforms: cased
  • workdocs.amazonaws.com transforms: cased
  • workmail.amazonaws.com transforms: cased
RiskLevelAggregatedeq
  • high transforms: cased
RiskLevelDuringSignIneq
  • high transforms: cased
timedefge
  • 0
timedefle
  • 8

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
IPAddresssummarize
RiskEventTypessummarize
RiskEventTypes_V2summarize
UserPrincipalNamesummarize
signInTimesummarize
timedefextend