← Back to SOC feed Coverage →

Guest account initiating privileged Entra ID operation

kql MEDIUM Azure-Sentinel
T1098.001T1098.003T1078.004
AuditLogsSigninLogs
backdoorcredential-thefthuntingmicrosoftofficialpersistence
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-28T11:00:00Z · Confidence: medium

Hunt Hypothesis

Guest accounts initiating privileged Entra ID operations may indicate a compromised account used for lateral movement or privilege escalation. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect potential unauthorized access and mitigate advanced persistent threats.

KQL Query

let timeframe = 1d;
let PrivilegedOps = dynamic([
    "Add member to role.",
    "Add member to role",
    "Add service principal credentials.",
    "Add service principal credentials",
    "Update application - Certificates and secrets management",
    "Add owner to service principal.",
    "Add owner to service principal",
    "Set domain authentication.",
    "Set domain authentication",
    "Add policy.",
    "Delete policy."
]);
// Guest accounts with successful sign-ins in the window
let GuestAccounts =
    SigninLogs
    | where TimeGenerated >= ago(timeframe)
    | where UserType =~ "Guest"
    | where ResultType == 0
    | extend UserUpn = tolower(UserPrincipalName)
    | where isnotempty(UserUpn)
    | summarize arg_max(TimeGenerated, IPAddress) by UserUpn
    | project UserUpn, LastSignIn = TimeGenerated, SignInIp = IPAddress;
// Privileged audit operations initiated by those guest accounts
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName in~ (PrivilegedOps)
| where Result =~ "success"
| extend ActorUpn = tolower(tostring(InitiatedBy.user.userPrincipalName))
| extend ActorIp  = iff(
      isnotempty(tostring(InitiatedBy.user.ipAddress)),
      tostring(InitiatedBy.user.ipAddress),
      tostring(InitiatedBy.app.ipAddress))
| where isnotempty(ActorUpn)
| join kind=inner GuestAccounts on $left.ActorUpn == $right.UserUpn
| extend AccountName      = tostring(split(ActorUpn, "@")[0])
| extend AccountUPNSuffix = tostring(split(ActorUpn, "@")[1])
| mv-expand TargetResource = TargetResources
| extend TargetName = tostring(TargetResource.displayName)
| summarize
    FirstSeen   = min(TimeGenerated),
    LastSeen    = max(TimeGenerated),
    Operations  = make_set(OperationName),
    Targets     = make_set(TargetName),
    EventCount  = dcount(CorrelationId)
    by ActorUpn, AccountName, AccountUPNSuffix, ActorIp, LastSignIn, SignInIp
| sort by FirstSeen desc

Analytic Rule Definition

id: bb135137-8f33-45b0-8d0f-78437f79d558
name: Guest account initiating privileged Entra ID operation
description: |
  Identifies guest accounts (B2B external identities) initiating high-impact Entra ID
  operations: role assignments, service principal credentials, policy changes. Guest
  as initiator suggests a compromised B2B account for persistence or lateral pivot.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
      - SigninLogs
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098.001
  - T1098.003
  - T1078.004
query: |
  let timeframe = 1d;
  let PrivilegedOps = dynamic([
      "Add member to role.",
      "Add member to role",
      "Add service principal credentials.",
      "Add service principal credentials",
      "Update application - Certificates and secrets management",
      "Add owner to service principal.",
      "Add owner to service principal",
      "Set domain authentication.",
      "Set domain authentication",
      "Add policy.",
      "Delete policy."
  ]);
  // Guest accounts with successful sign-ins in the window
  let GuestAccounts =
      SigninLogs
      | where TimeGenerated >= ago(timeframe)
      | where UserType =~ "Guest"
      | where ResultType == 0
      | extend UserUpn = tolower(UserPrincipalName)
      | where isnotempty(UserUpn)
      | summarize arg_max(TimeGenerated, IPAddress) by UserUpn
      | project UserUpn, LastSignIn = TimeGenerated, SignInIp = IPAddress;
  // Privileged audit operations initiated by those guest accounts
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName in~ (PrivilegedOps)
  | where Result =~ "success"
  | extend ActorUpn = tolower(tostring(InitiatedBy.user.userPrincipalName))
  | extend ActorIp  = iff(
        isnotempty(tostring(InitiatedBy.user.ipAddress)),
        tostring(InitiatedBy.user.ipAddress),
        tostring(InitiatedBy.app.ipAddress))
  | where isnotempty(ActorUpn)
  | join kind=inner GuestAccounts on $left.ActorUpn == $right.UserUpn
  | extend AccountName      = tostring(split(ActorUpn, "@")[0])
  | extend AccountUPNSuffix = tostring(split(ActorUpn, "@")[1])
  | mv-expand TargetResource = TargetResources
  | extend TargetName = tostring(TargetResource.displayName)
  | summarize
      FirstSeen   = min(TimeGenerated),
      LastSeen    = max(TimeGenerated),
      Operations  = make_set(OperationName),
      Targets     = make_set(TargetName),
      EventCount  = dcount(CorrelationId)
      by ActorUpn, AccountName, AccountUPNSuffix, ActorIp, LastSignIn, SignInIp
  | sort by FirstSeen desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ActorIp
version: 1.0.0
metadata:
    source:
        ki

Required Data Sources

Sentinel TableNotes
AuditLogsEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/MultipleDataSources/GuestAccountPrivilegedOperation.yaml