Detection rules › Kusto
Account Created and Deleted in Short Timeframe
Search for user principal name (UPN) events. Look for accounts created and then deleted in under 24 hours. Attackers may create an account for their use, and then remove the account when no longer needed. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
Event coverage
| Provider | Event |
|---|---|
| Entra-AuditLogs | Add user |
| Entra-AuditLogs | Delete user |
Rule body kusto
id: bb616d82-108f-47d3-9dec-9652ea0d3bf6
name: Account Created and Deleted in Short Timeframe
description: |
'Search for user principal name (UPN) events. Look for accounts created and then deleted in under 24 hours. Attackers may create an account for their use, and then remove the account when no longer needed.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#short-lived-account'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let queryfrequency = 1h;
let queryperiod = 7d; // Increased queryperiod to 7 days to mitigate timing-based bypasses
AuditLogs
| where TimeGenerated > ago(queryfrequency)
| where OperationName =~ "Delete user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
// Making extraction case-insensitive, and converting the result to lowercase for consistency
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
// Standardize UserId to ensure consistent, immutable identification
| extend UserId = tostring(TargetResource.id)
)
| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName =~ "Add user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
// Normalize TargetUserPrincipalName by removing optional 32-char hex prefixes and underscores
// Making extraction case-insensitive, and converting the result to lowercase for consistency
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
// Standardize UserId to ensure consistent, immutable identification
| extend UserId = tostring(TargetResource.id)
)
| project-rename Creation_TimeGenerated = TimeGenerated
) on UserId
| extend TimeDelta = Deletion_TimeGenerated - Creation_TimeGenerated
| where TimeDelta between (time(0s) .. queryperiod)
| extend CreatedByApp = tostring(InitiatedBy.app.displayName),
CreatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
CreatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
CreatedByAadUserId = tostring(InitiatedBy.user.id),
CreatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
| project Creation_TimeGenerated, Deletion_TimeGenerated, TimeDelta, UserId, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
CreatedByApp, CreatedByAppServicePrincipalId, CreatedByUserPrincipalName, CreatedByAadUserId, CreatedByIPAddress, Creation_AdditionalDetails = AdditionalDetails, Creation_InitiatedBy = InitiatedBy, Creation_TargetResources = TargetResources, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend CreatedByName = tostring(split(CreatedByUserPrincipalName,'@',0)[0]), CreatedByUPNSuffix = tostring(split(CreatedByUserPrincipalName,'@',1)[0])
| extend DeletedByName = tostring(split(DeletedByUserPrincipalName,'@',0)[0]), DeletedByUPNSuffix = tostring(split(DeletedByUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: CreatedByUserPrincipalName
- identifier: Name
columnName: CreatedByName
- identifier: UPNSuffix
columnName: CreatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: CreatedByAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: DeletedByUserPrincipalName
- identifier: Name
columnName: DeletedByName
- identifier: UPNSuffix
columnName: DeletedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: DeletedByAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CreatedByIPAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DeletedByIPAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Parameters
let queryfrequency = 1h;
let queryperiod = 7d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(queryfrequency)
Stage 3: where
| where OperationName =~ "Delete user"
Stage 4: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
| extend UserId = tostring(TargetResource.id)
)
Stage 5: extend
| extend DeletedByApp = tostring(InitiatedBy.app.displayName),
DeletedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
DeletedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
DeletedByAadUserId = tostring(InitiatedBy.user.id),
DeletedByIPAddress = tostring(InitiatedBy.user.ipAddress)
Stage 6: project
| project Deletion_TimeGenerated = TimeGenerated, TargetUserPrincipalName, UserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
Deletion_AdditionalDetails = AdditionalDetails, Deletion_InitiatedBy = InitiatedBy, Deletion_TargetResources = TargetResources
Stage 7: join
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName =~ "Add user"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type == "User"
| extend TargetUserPrincipalName = tolower(extract(@"(?i)([a-f0-9]{32})?_?(.*)", 2, tostring(TargetResource.userPrincipalName)))
| extend UserId = tostring(TargetResource.id)
)
| project-rename Creation_TimeGenerated = TimeGenerated
) on UserId
Stage 8: extend
| extend TimeDelta = Deletion_TimeGenerated - Creation_TimeGenerated
Stage 9: where
| where TimeDelta between (time(0s) .. queryperiod)
Stage 10: extend
| extend CreatedByApp = tostring(InitiatedBy.app.displayName),
CreatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId),
CreatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName),
CreatedByAadUserId = tostring(InitiatedBy.user.id),
CreatedByIPAddress = tostring(InitiatedBy.user.ipAddress)
Stage 11: project
| project Creation_TimeGenerated, Deletion_TimeGenerated, TimeDelta, UserId, TargetUserPrincipalName, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByUserPrincipalName, DeletedByAadUserId, DeletedByIPAddress,
CreatedByApp, CreatedByAppServicePrincipalId, CreatedByUserPrincipalName, CreatedByAadUserId, CreatedByIPAddress, Creation_AdditionalDetails = AdditionalDetails, Creation_InitiatedBy = InitiatedBy, Creation_TargetResources = TargetResources, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources
Stage 12: extend (3 consecutive steps)
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend CreatedByName = tostring(split(CreatedByUserPrincipalName,'@',0)[0]), CreatedByUPNSuffix = tostring(split(CreatedByUserPrincipalName,'@',1)[0])
| extend DeletedByName = tostring(split(DeletedByUserPrincipalName,'@',0)[0]), DeletedByUPNSuffix = tostring(split(DeletedByUserPrincipalName,'@',1)[0])
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.
| Field | Kind | Values |
|---|---|---|
OperationName | eq |
|
type | eq |
|
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.
| Field | Source |
|---|---|
CreatedByAadUserId | project |
CreatedByApp | project |
CreatedByAppServicePrincipalId | project |
CreatedByIPAddress | project |
CreatedByUserPrincipalName | project |
Creation_AdditionalDetails | project |
Creation_InitiatedBy | project |
Creation_TargetResources | project |
Creation_TimeGenerated | project |
DeletedByAadUserId | project |
DeletedByApp | project |
DeletedByAppServicePrincipalId | project |
DeletedByIPAddress | project |
DeletedByUserPrincipalName | project |
Deletion_AdditionalDetails | project |
Deletion_InitiatedBy | project |
Deletion_TargetResources | project |
Deletion_TimeGenerated | project |
TargetUserPrincipalName | project |
TimeDelta | project |
UserId | project |
TargetName | extend |
TargetUPNSuffix | extend |
CreatedByName | extend |
CreatedByUPNSuffix | extend |
DeletedByName | extend |
DeletedByUPNSuffix | extend |