Detection rules › Kusto
Suspicious Login from deleted guest account
This query will detect logins from guest account which was recently deleted. For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Delete user |
| Entra ID | SigninLogs sign-in event: Successful sign-in |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
Rule body
id: defe4855-0d33-4362-9557-009237623976
name: Suspicious Login from deleted guest account
description: |
' This query will detect logins from guest account which was recently deleted.
For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- GuestorExternalIdentities
query: |
let query_frequency = 1h;
let query_period = 1d;
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
| mv-expand TargetResource = TargetResources
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
| extend
Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(query_period)
| where ResultType == 0
| summarize take_any(*) by UserPrincipalName
| extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
| project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
| where SigninLogs_TimeGenerated > Delete_TimeGenerated
| project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
| extend
AccountName = tostring(split(UserPrincipalName, "@")[0]),
AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let query_frequency = 1h;
let query_period = 1d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(query_frequency)
Stage 3: where
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
Stage 4: mv-expand
| mv-expand TargetResource = TargetResources
Stage 5: where
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
Stage 6: extend
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
Stage 7: extend
| extend
Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
Stage 8: project
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
Stage 9: join
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(query_period)
| where ResultType == 0
| summarize take_any(*) by UserPrincipalName
| extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
| project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
Stage 10: where
| where SigninLogs_TimeGenerated > Delete_TimeGenerated
Stage 11: project-away
| project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
Stage 12: extend
| extend
AccountName = tostring(split(UserPrincipalName, "@")[0]),
AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Stage 13: summarize aggregation inside the join branch
summarize by UserPrincipalName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"UserManagement" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Delete user" |
ResultType | eq |
| field:"ResultType" kind:eq value:"0" |
SigninLogs_TimeGenerated | cross_field_compare |
| field:"SigninLogs_TimeGenerated" kind:cross_field_compare value:"Delete_TimeGenerated" |
type | eq |
| field:"type" kind:eq value:"User" |
userPrincipalName | match |
| field:"userPrincipalName" kind:match value:"#EXT#" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserPrincipalName | summarize |