← Back to SOC feed Coverage →

Tracking Privileged Account Rare Activity

kql MEDIUM Azure-Sentinel
T1078T1087
OfficeActivitySecurityAlertSecurityEventSigninLogsW3CIISLog
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-06-03T23:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may be using high-value accounts with rare activity to move laterally or execute privileged actions undetected. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential privilege escalation or persistent access attempts.

KQL Query

let LocalSID = "S-1-5-32-5[0-9][0-9]$";
let GroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$";
let p_Accounts = SecurityEvent
| where EventID in ("4728", "4732", "4756") and AccountType == "User" and MemberName == "-"
// Exclude Remote Desktop Users group: S-1-5-32-555 and IIS Users group S-1-5-32-568
| where TargetSid !in ("S-1-5-32-555", "S-1-5-32-568")
| where TargetSid matches regex LocalSID or TargetSid matches regex GroupSID
| summarize by DomainSlashAccount = tolower(SubjectAccount), NtDomain = SubjectDomainName,
AccountAtDomain = tolower(strcat(SubjectUserName,"@",SubjectDomainName)), AccountName = tolower(SubjectUserName);
// Build custom high value account list
let cust_Accounts = datatable(Account:string, NtDomain:string, Domain:string)[
"john", "Contoso", "contoso.com",  "greg", "Contoso", "contoso.com",  "larry", "Domain", "contoso.com"];
let c_Accounts = cust_Accounts
| extend AccountAtDomain = tolower(strcat(Account,"@",Domain)), AccountName = tolower(Account),
DomainSlashAccount = tolower(strcat(NtDomain,"\\",Account));
let AccountFormat = p_Accounts | union c_Accounts | project AccountName, AccountAtDomain, DomainSlashAccount;
// Normalize activity from diverse sources into common schema using a function
let activity = view (a_StartTime:datetime, a_EndTime:datetime) {
(union isfuzzy=true
(AccountFormat | join kind=inner
(AWSCloudTrail
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend ClientIP = "-", AccountName = tolower(UserIdentityUserName), WinSecEventDomain = "-"
| project-rename EventType = EventName, ServiceOrSystem = EventSource)
on AccountName),
(AccountFormat | join kind=inner
(SigninLogs
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend AccountName = tolower(split(UserPrincipalName, "@")[0]), WinSecEventDomain = "-"
| extend Event = strcat("OperationName", "-", "ResultType", "-", "ResultDescription")
| project-rename EventType = Event, ServiceOrSystem = AppDisplayName, ClientIP = IPAddress)
on AccountName),
(AccountFormat | join kind=inner
(OfficeActivity
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend AccountName = tolower(split(UserId, "@")[0]), WinSecEventDomain = "-"
| extend Event = strcat(Operation, "-", ResultStatus)
| project-rename EventType = Event, ServiceOrSystem = OfficeWorkload)
 on AccountName),
(AccountFormat | join kind=inner
(SecurityEvent
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where EventID in (4624, 4625)
| extend ClientIP = "-"
| extend AccountName = tolower(split(Account,"\\")[1]), Domain = tolower(split(Account,"\\")[0])
| project-rename EventType = Activity, ServiceOrSystem = Computer, WinSecEventDomain = Domain)
on AccountName),
(AccountFormat | join kind=inner
(W3CIISLog
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where csUserName != "-" and isnotempty(csUserName)
| extend AccountName = tolower(csUserName), WinSecEventDomain = "-"
| project-rename EventType = csMethod, ServiceOrSystem = sSiteName, ClientIP = cIP)
on AccountName),
(AccountFormat | join kind=inner
(W3CIISLog
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where csUserName != "-" and isnotempty(csUserName)
| extend AccountAtDomain = tolower(csUserName), WinSecEventDomain = "-"
| project-rename EventType = csMethod, ServiceOrSystem = sSiteName, ClientIP = cIP)
on AccountAtDomain));
};
// Rare activity today versus prior week
let LastDay = startofday(ago(1d));
let PrevDay = endofday(ago(2d));
let Prev7Day = startofday(ago(8d));
let ra_LastDay = activity(LastDay, now())
| summarize ra_StartTime = min(TimeGenerated), ra_EndTime = max(TimeGenerated),
ra_Count = count() by Type, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let a_7day = activity(Prev7Day, PrevDay)
| summarize ha_Count = count() by Type, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let ra_Today = ra_LastDay | join kind=leftanti (a_7day) on Type, AccountName, ServiceOrSystem
| extend RareServiceOrSystem = ServiceOrSystem;
// Retrieve related activity as context
let a_Related =
(union isfuzzy=true
(// Make sure we at least publish the unusual activity we identified above - even if no related context activity is found in the subsequent union
ra_Today),
// Remaining elements of the union look for related activity
(ra_Today | join kind=inner
(OfficeActivity
| where TimeGenerated > LastDay
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(OfficeWorkload),
rel_ServiceOrSystemSet = makeset(OfficeWorkload), rel_ClientIPSet = makeset(ClientIP),
rel_Count = count() by AccountName = tolower(UserId), rel_EventType = Operation, Type
) on AccountName),
(ra_Today | join kind=inner
(SecurityEvent | where TimeGenerated > LastDay
| where EventID in (4624, 4625)
| where AccountType == "User"
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(Computer),
rel_ServiceOrSystemSet = makeset(Computer), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(Account), rel_EventType = Activity, Type, AccountName
) on AccountName),
(ra_Today | join kind=inner
(Event | where TimeGenerated > LastDay
// 7045: A service was installed in the system
| where EventID == 7045
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(Computer),
rel_ServiceOrSystemSet = makeset(Computer), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(UserName), rel_EventType = strcat(EventID, "-", tostring(split(RenderedDescription,".")[0])), Type
) on $left.AccountName == $right.DomainSlashAccount),
(ra_Today | join kind=inner
(SecurityEvent | where TimeGenerated > LastDay
// 4720: Account created, 4726: Account deleted
| where EventID in (4720,4726)
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(UserPrincipalName),
rel_ServiceOrSystemSet = makeset(UserPrincipalName), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(Account), rel_EventType = Activity, Type, AccountName
) on AccountName),
(ra_Today | join kind=inner
(SigninLogs | where TimeGenerated > LastDay
| extend RemoteHost = tolower(tostring(parse_json(DeviceDetail.["displayName"])))
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser, StatusCode = tostring(Status.errorCode),
StatusDetails = tostring(Status.additionalDetails), State = tostring(LocationDetails.state)
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), a_RelatedRemoteHostSet = makeset(RemoteHost),
rel_ServiceOrSystemSet = makeset(AppDisplayName), rel_ServiceOrSystemCount = dcount(AppDisplayName), rel_ClientIPSet = makeset(IPAddress),
rel_StateSet = makeset(State),
rel_Count = count() by AccountAtDomain = tolower(UserPrincipalName), rel_EventType = iff(isnotempty(ResultDescription), ResultDescription, StatusDetails), Type)
 on $left.WinSecEventDomain == $right.AccountAtDomain),
(ra_Today | join kind=inner
(AWSCloudTrail | where TimeGenerated > LastDay
| summarize rel_StartTime = min(TimeGenerated),rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemSet = makeset(EventSource),
rel_ServiceOrSystemCount = dcount(EventSource), rel_ClientIPSet = makeset("-"),
rel_Count= count() by AccountName = tolower(UserIdentityUserName), rel_EventType = EventName, Type
) on AccountName),
(ra_Today | join kind=inner
(SecurityAlert | where TimeGenerated > LastDay
| extend ExtProps=parse_json(ExtendedProperties)
| extend AccountName = tostring(ExtProps.["user name"])
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(AlertType),
rel_ServiceOrSystemSet = makeset(AlertType),
rel_Count = count() by DomainSlashAccount = tolower(AccountName), rel_EventType = ProductName, Type, AccountName) 
on AccountName)
);
a_Related
| project Type, RareActivtyStartTimeUtc = ra_StartTime, RareActivityEndTimeUtc = ra_EndTime, RareActivityCount = ra_Count,
AccountName, WinSecEventDomain, EventType, RareServiceOrSystem, RelatedActivityStartTimeUtc = rel_StartTime,
RelatedActivityEndTimeUtc = rel_EndTime, RelatedActivityEventType = rel_EventType, RelatedActivityClientIPSet = rel_ClientIPSet,
RelatedActivityServiceOrSystemCount = rel_ServiceOrSystemCount, RelatedActivityServiceOrSystemSet = rel_ServiceOrSystemSet, RelatedActivityCount = rel_Count
| extend timestamp = RareActivtyStartTimeUtc, AccountCustomEntity = AccountName

Analytic Rule Definition

id: 431cccd3-2dff-46ee-b34b-61933e45f556
name: Tracking Privileged Account Rare Activity
description: |
  'This query determines rare activity by a high-value account on a system or service. If any account with rare activity is found, the query retrieves related activity from that account on the same day and summarizes the information.'
description_detailed: |
  'This query will determine rare activity by a high-value account carried out on a system or service.
  High Value accounts are determined by Group Membership to High Value groups via events listed below.
  Rare here means an activity type seen in the last day which has not been seen in the previous 7 days.
  If any account with such rare activity is found, the query will attempt to retrieve related activity
  from that account on that same day and summarize the information.
  4728 - A member was added to a security-enabled global group
  4732 - A member was added to a security-enabled local group
  4756 - A member was added to a security-enabled universal group'
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
tactics:
  - PrivilegeEscalation
  - Discovery
relevantTechniques:
  - T1078
  - T1087
query: |
  let LocalSID = "S-1-5-32-5[0-9][0-9]$";
  let GroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$";
  let p_Accounts = SecurityEvent
  | where EventID in ("4728", "4732", "4756") and AccountType == "User" and MemberName == "-"
  // Exclude Remote Desktop Users group: S-1-5-32-555 and IIS Users group S-1-5-32-568
  | where TargetSid !in ("S-1-5-32-555", "S-1-5-32-568")
  | where TargetSid matches regex LocalSID or TargetSid matches regex GroupSID
  | summarize by DomainSlashAccount = tolower(SubjectAccount), NtDomain = SubjectDomainName,
  AccountAtDomain = tolower(strcat(SubjectUserName,"@",SubjectDomainName)), AccountName = tolower(SubjectUserName);
  // Build custom high value account list
  let cust_Accounts = datatable(Account:string, NtDomain:string, Domain:string)[
  "john", "Contoso", "contoso.com",  "greg", "Contoso", "contoso.com",  "larry", "Domain", "contoso.com"];
  let c_Accounts = cust_Accounts
  | extend AccountAtDomain = tolower(strcat(Account,"@",Domain)), AccountName = tolower(Account),
  DomainSlashAccount = tolower(strcat(NtDomain,"\\",Account));
  let AccountFormat = p_Accounts | union c_Accounts | project AccountName, AccountAtDomain, DomainSlashAccount;
  // Normalize activity from diverse sources into common schema using a function
  let activity = view (a_StartTime:datetime, a_EndTime:datetime) {
  (union isfuzzy=true
  (AccountFormat | join kind=inner
  (AWSCloudTrail
  | where TimeGene

Required Data Sources

Sentinel TableNotes
OfficeActivityEnsure this data connector is enabled
SecurityAlertEnsure this data connector is enabled
SecurityEventEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled
W3CIISLogEnsure 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/MultipleDataSources/TrackingPrivAccounts.yaml