Detection rules › Kusto

Account Created and Deleted in Short Timeframe

Status
available
Severity
high
Time window
7d
Group by
UserId
Source
github.com/Azure/Azure-Sentinel

Search for user principal name (UPN) events. Look for accounts created and then deleted within 7 days. 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

TacticTechniques
Initial Access

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

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 within 7 days. 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.2
kind: Scheduled

Stages and Predicates

Stage 0: let

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

Stage 9: where

where /* macro: (TimeDelta between (time(0s) .. 7d)) */

Stage 10: extend

extend CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName

Stage 11: project

project CreatedByAadUserId, CreatedByApp, CreatedByAppServicePrincipalId, CreatedByIPAddress, CreatedByUserPrincipalName, Creation_AdditionalDetails, Creation_InitiatedBy, Creation_TargetResources, Creation_TimeGenerated, DeletedByAadUserId, DeletedByApp, DeletedByAppServicePrincipalId, DeletedByIPAddress, DeletedByUserPrincipalName, Deletion_AdditionalDetails, Deletion_InitiatedBy, Deletion_TargetResources, Deletion_TimeGenerated, TargetUserPrincipalName, TimeDelta, UserId

Stage 12: extend (3 consecutive steps)

extend CreatedByName, CreatedByUPNSuffix, DeletedByName, DeletedByUPNSuffix, TargetName, TargetUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
CreatedByAadUserIdproject
CreatedByAppproject
CreatedByAppServicePrincipalIdproject
CreatedByIPAddressproject
CreatedByUserPrincipalNameproject
Creation_AdditionalDetailsproject
Creation_InitiatedByproject
Creation_TargetResourcesproject
Creation_TimeGeneratedproject
DeletedByAadUserIdproject
DeletedByAppproject
DeletedByAppServicePrincipalIdproject
DeletedByIPAddressproject
DeletedByUserPrincipalNameproject
Deletion_AdditionalDetailsproject
Deletion_InitiatedByproject
Deletion_TargetResourcesproject
Deletion_TimeGeneratedproject
TargetUserPrincipalNameproject
TimeDeltaproject
UserIdproject
TargetNameextend
TargetUPNSuffixextend
CreatedByNameextend
CreatedByUPNSuffixextend
DeletedByNameextend
DeletedByUPNSuffixextend