Detection rules › Kusto

User account created without expected attributes defined

Severity
low
Time window
1d
Group by
CorrelationId, CreatedUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, CreatingUserPrincipalName, TenantId, TimeGenerated, displayName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query looks for accounts being created that do not have attributes populated that are commonly populated in the tenant. Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts. Created accounts should be investigated to ensure they were legitimated created. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1136.003 Create Account: Cloud Account

Event coverage

Rule body kusto

id: dc99e38c-f4e9-4837-94d7-353ac0b01a77
name: User account created without expected attributes defined
description: |
  'This query looks for accounts being created that do not have attributes populated that are commonly populated in the tenant.
    Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts.
    Created accounts should be investigated to ensure they were legitimated created.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1136.003
tags:
  - AADSecOpsGuide
query: |
    let threshold = 10;
    let default_ad_attributes = dynamic(["LastDirSyncTime", "StsRefreshTokensValidFrom", "Included Updated Properties", "AccountEnabled", "Action Client Name", "SourceAnchor"]);
    let addUsers = AuditLogs
    | where OperationName =~ "Add user"
    | where Result =~ "success"
    | extend AccountProperties = TargetResources[0].modifiedProperties
    | mv-expand AccountProperties
    ;
    addUsers
    | evaluate bag_unpack(AccountProperties) : (displayName:string, oldValue: string, newValue: string , TenantId : string, SourceSystem : string, TimeGenerated : datetime, ResourceId : string, OperationName : string, OperationVersion : string, Category : string, ResultType : string, ResultSignature : string, ResultDescription : string, DurationMs : long, CorrelationId : string, Resource : string, ResourceGroup : string, ResourceProvider : string, Identity : string, Level : string, Location : string, AdditionalDetails : dynamic, Id : string, InitiatedBy : dynamic, LoggedByService : string, Result : string, ResultReason : string, TargetResources : dynamic, AADTenantId : string, ActivityDisplayName : string, ActivityDateTime : datetime, AADOperationType : string, Type : string)
    | extend displayName = column_ifexists("displayName", "Unknown Value")
    | summarize count() by displayName, TenantId
    | where displayName !in (default_ad_attributes)
    | top threshold by count_ desc
    | summarize make_set(displayName) by TenantId
    | join kind=inner (
    addUsers
    | extend CreatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend CreatingAadUserId = tostring(InitiatedBy.user.id)
    | extend CreatingUserIPAddress = tostring(InitiatedBy.user.ipAddress)
    | extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
    | extend PropName = tostring(AccountProperties.displayName)) 
    on TenantId
    | summarize makeset(PropName) by TimeGenerated, CorrelationId, CreatedUserPrincipalName, CreatingUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, tostring(set_displayName)
    | extend missing_props = set_difference(todynamic(set_displayName), set_PropName)
    | where array_length(missing_props) > 0
    | join kind=innerunique (
    AuditLogs
    | where Result =~ "success"
    | where OperationName =~ "Add user"
    | extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)) 
    on CorrelationId, CreatedUserPrincipalName
    | extend ExpectedProperties = set_displayName
    | project-away set_displayName, set_PropName
    | extend InitiatingAccountName = tostring(split(CreatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(CreatingUserPrincipalName, "@")[1])
    | extend TargetAccountName = tostring(split(CreatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(CreatedUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: CreatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatedUserPrincipalName
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CreatingUserIPAddress
version: 1.1.0
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let threshold = 10;
let default_ad_attributes = dynamic(["LastDirSyncTime", "StsRefreshTokensValidFrom", "Included Updated Properties", "AccountEnabled", "Action Client Name", "SourceAnchor"]);

The stages below define let addUsers (the rule's main pipeline source).

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName =~ "Add user"

Stage 3: where

| where Result =~ "success"

Stage 4: extend

| extend AccountProperties = TargetResources[0].modifiedProperties

Stage 5: mv-expand

| mv-expand AccountProperties

The stages below run on addUsers (the outer pipeline).

Stage 6: evaluate

addUsers
| evaluate bag_unpack(AccountProperties) : (displayName:string, oldValue: string, newValue: string , TenantId : string, SourceSystem : string, TimeGenerated : datetime, ResourceId : string, OperationName : string, OperationVersion : string, Category : string, ResultType : string, ResultSignature : string, ResultDescription : string, DurationMs : long, CorrelationId : string, Resource : string, ResourceGroup : string, ResourceProvider : string, Identity : string, Level : string, Location : string, AdditionalDetails : dynamic, Id : string, InitiatedBy : dynamic, LoggedByService : string, Result : string, ResultReason : string, TargetResources : dynamic, AADTenantId : string, ActivityDisplayName : string, ActivityDateTime : datetime, AADOperationType : string, Type : string)

Stage 7: extend

| extend displayName = column_ifexists("displayName", "Unknown Value")

Stage 8: summarize

| summarize count() by displayName, TenantId

Stage 9: where

| where displayName !in (default_ad_attributes)

Stage 10: top

| top threshold by count_ desc

Stage 11: summarize

| summarize make_set(displayName) by TenantId

Stage 12: join

| join kind=inner (
addUsers
| extend CreatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatingAadUserId = tostring(InitiatedBy.user.id)
| extend CreatingUserIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
| extend PropName = tostring(AccountProperties.displayName)) 
on TenantId

Stage 13: summarize

| summarize makeset(PropName) by TimeGenerated, CorrelationId, CreatedUserPrincipalName, CreatingUserPrincipalName, CreatingAadUserId, CreatingUserIPAddress, tostring(set_displayName)

Stage 14: extend

| extend missing_props = set_difference(todynamic(set_displayName), set_PropName)

Stage 15: where

| where array_length(missing_props) > 0

Stage 16: join

| join kind=innerunique (
AuditLogs
| where Result =~ "success"
| where OperationName =~ "Add user"
| extend CreatedUserPrincipalName = tostring(TargetResources[0].userPrincipalName)) 
on CorrelationId, CreatedUserPrincipalName

Stage 17: extend

| extend ExpectedProperties = set_displayName

Stage 18: project-away

| project-away set_displayName, set_PropName

Stage 19: extend

| extend InitiatingAccountName = tostring(split(CreatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(CreatingUserPrincipalName, "@")[1])

Stage 20: extend

| extend TargetAccountName = tostring(split(CreatedUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(CreatedUserPrincipalName, "@")[1])

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
displayNameinAccountEnabled, Action Client Name, Included Updated Properties, LastDirSyncTime, SourceAnchor, StsRefreshTokensValidFrom

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
OperationNameeq
  • Add user
Resulteq
  • success
missing_propsgt
  • 0 transforms: array_length

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
CorrelationIdsummarize
CreatedUserPrincipalNamesummarize
CreatingAadUserIdsummarize
CreatingUserIPAddresssummarize
CreatingUserPrincipalNamesummarize
TimeGeneratedsummarize
missing_propsextend
ExpectedPropertiesextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetAccountUPNSuffixextend