← Back to SOC feed Coverage →

Rare Audit activity initiated by User

kql MEDIUM Azure-Sentinel
T1136
AuditLogs
huntingmicrosoftofficial
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-04-23T09:00:00Z · Confidence: medium

Hunt Hypothesis

Unusual audit activity initiated by a single user may indicate an attempt to manipulate access controls or escalate privileges. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect potential insider threats or unauthorized changes to user/group permissions.

KQL Query


let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let auditLookback = starttime - 14d;
let propertyIgnoreList = dynamic(["TargetId.UserType", "StsRefreshTokensValidFrom", "LastDirSyncTime", "DeviceOSVersion", "CloudDeviceOSVersion", "DeviceObjectVersion"]);
let AuditTrail = AuditLogs 
| where TimeGenerated >= auditLookback and TimeGenerated < starttime
| where isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend InitiatedByIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| extend ModProps = TargetResources.[0].modifiedProperties
| extend TargetUserPrincipalName = tolower(tostring(TargetResources.[0].userPrincipalName))
| extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
| mv-expand ModProps
| extend PropertyName = tostring(ModProps.displayName), newValue = tostring(parse_json(tostring(ModProps.newValue))[0])
| where PropertyName !in~ (propertyIgnoreList) and (PropertyName !~ "Action Client Name" and newValue !~ "DirectorySync") and (PropertyName !~ "Included Updated Properties" and newValue !~ "LastDirSyncTime")
| summarize count() by OperationName, InitiatedByUser, InitiatedByIPAddress, TargetUserPrincipalName, PropertyName, TargetResourceName;
let AccountMods = AuditLogs 
| where TimeGenerated >= starttime
| where isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend InitiatedByIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
| extend ModProps = TargetResources.[0].modifiedProperties
| extend TargetUserPrincipalName = tolower(tostring(TargetResources.[0].userPrincipalName))
| extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
| mv-expand ModProps
| extend PropertyName = tostring(ModProps.displayName), newValue = tostring(parse_json(tostring(ModProps.newValue))[0])
| where PropertyName !in~ (propertyIgnoreList) and (PropertyName !~ "Action Client Name" and newValue !~ "DirectorySync") and (PropertyName !~ "Included Updated Properties" and newValue !~ "LastDirSyncTime")
| extend ModifiedProps = pack("PropertyName",PropertyName,"newValue",newValue, "Id", Id, "CorrelationId", CorrelationId) 
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Activity = make_bag(ModifiedProps) by Type, InitiatedByUser, InitiatedByIPAddress, TargetUserPrincipalName, Category, OperationName, PropertyName, TargetResourceName;
let RareAudits = AccountMods | join kind= leftanti (
   AuditTrail 
) on OperationName, InitiatedByUser, InitiatedByIPAddress;//, TargetUserPrincipalName, PropertyName; //uncomment if you want to see Rare Property changes to a given TargetUserPrincipalName.
RareAudits 
| summarize StartTime = min(StartTimeUtc), EndTime = max(EndTimeUtc), make_set(Activity), make_set(PropertyName) by Type, InitiatedByUser, InitiatedByIPAddress, OperationName, TargetUserPrincipalName, TargetResourceName
| order by InitiatedByUser asc, StartTime asc
| extend timestamp = StartTime, AccountCustomEntity = InitiatedByUser, HostCustomEntity = iff(set_PropertyName has_any ('DeviceOSType', 'CloudDeviceOSType'), TargetResourceName, ''), IPCustomEntity = InitiatedByIPAddress

Analytic Rule Definition

id: ea107ccc-2b80-410e-96e1-be6607ce293b
name: Rare Audit activity initiated by User
description: |
  'Compares current day to last 14 days of audits to identify new audit activities. Useful for tracking malicious activity related to user/group additions/removals by specific users.'
description_detailed: |
  'Compares the current day to the last 14 days of audits to identify new audit activities by 
  OperationName, InitiatedByUser, UserPrincipalName, PropertyName, newValue
  This can be useful when attempting to track down malicious activity related to additions of 
  new users, additions to groups, removal from groups by specific users.'
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - LateralMovement
relevantTechniques:
  - T1136
query: |

  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let auditLookback = starttime - 14d;
  let propertyIgnoreList = dynamic(["TargetId.UserType", "StsRefreshTokensValidFrom", "LastDirSyncTime", "DeviceOSVersion", "CloudDeviceOSVersion", "DeviceObjectVersion"]);
  let AuditTrail = AuditLogs 
  | where TimeGenerated >= auditLookback and TimeGenerated < starttime
  | where isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
  | extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend InitiatedByIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
  | extend ModProps = TargetResources.[0].modifiedProperties
  | extend TargetUserPrincipalName = tolower(tostring(TargetResources.[0].userPrincipalName))
  | extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
  | mv-expand ModProps
  | extend PropertyName = tostring(ModProps.displayName), newValue = tostring(parse_json(tostring(ModProps.newValue))[0])
  | where PropertyName !in~ (propertyIgnoreList) and (PropertyName !~ "Action Client Name" and newValue !~ "DirectorySync") and (PropertyName !~ "Included Updated Properties" and newValue !~ "LastDirSyncTime")
  | summarize count() by OperationName, InitiatedByUser, InitiatedByIPAddress, TargetUserPrincipalName, PropertyName, TargetResourceName;
  let AccountMods = AuditLogs 
  | where TimeGenerated >= starttime
  | where isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
  | extend InitiatedByUser = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend InitiatedByIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
  | extend ModProps = TargetResources.[0].modifiedProperties
  | extend TargetUserPrincipalName = tolower(tostring(TargetResources.[0].userPrincipalName))
  | extend TargetResourceName = tolower(tostring(TargetResources.[0].displayName))
  | mv-expand ModProps
  | extend PropertyName = tostring(ModProps.displayName), newValue = tostring(parse_json(tostring(ModProps.newValue))[0])
  | where PropertyName !i

Required Data Sources

Sentinel TableNotes
AuditLogsEnsure 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/AuditLogs/RareAuditActivityByUser.yaml