5 LogScale queries to audit Active Directory group changes
Five practical LogScale queries for tracking who added or removed members from privileged AD groups.
Changes to membership in privileged Active Directory groups like Domain Admins, Enterprise Admins, and Backup Operators are usually among the first things an attacker targets after gaining access — a foothold on a low-privilege account is far less useful than a foothold with domain-wide rights, and group membership is often the fastest path from one to the other. At the same time, these changes are some of the easiest things to miss: they’re a handful of specific event IDs buried among the much larger volume of everyday Windows Security logs, and unless you’re specifically looking for them, they tend to scroll past unnoticed until an audit or an incident forces the question.
The five queries below start broad and get progressively more targeted, moving from “show me everything” to specific patterns worth alerting on to a rollup suitable for reporting. They assume Windows Security events are sent to LogScale using the standard #windows.EventID field — if you’re ingesting through a custom parser or a different field mapping, adjust the field names accordingly before running them.
One thing worth knowing before you start: MemberName is not reliably populated in these events. Depending on the source and the parser, it arrives as a full distinguished name, as a bare account name, or as nothing at all — while MemberSid is always present. Every table below therefore carries both, so an empty member column is a parser detail rather than a missing result.
1. Track every group membership change, no matter which group it is
Event IDs 4728 and 4729 (global groups), 4732 and 4733 (local groups), and 4756 and 4757 (universal groups) fire whenever a member is added to or removed from a group, regardless of which group it is or who made the change. Combining all six into a single view gives you the full stream of membership activity across the domain, which is the right starting point before you narrow down to anything specific.
This one is deliberately broad. It’s not meant to be alerted on directly — with any reasonably active AD environment, group membership changes happen constantly for mundane reasons — but it’s the query you reach for when you’re investigating an incident and need to see everything that touched group membership around a given time window, or when you want a quick sanity check that the event forwarding pipeline is actually capturing these IDs at all.
in(field="#windows.EventID", values=[4732,4728,4756,4729,4733,4757])
| case {
in(field="#windows.EventID", values=[4732,4728,4756]) | action:= "added";
in(field="#windows.EventID", values=[4733,4729,4757]) | action:= "removed";
}
| rename(field="windows.EventData.TargetUserName", as="group")
| rename(field="windows.EventData.MemberName", as="member")
| rename(field="windows.EventData.MemberSid", as="member_sid")
| rename(field="windows.EventData.SubjectUserName", as="administrator")
| table([@timestamp, action, group, member, member_sid, administrator])
| sort(@timestamp, order=desc)
Track every Active Directory group membership change on mylogscale.
The result is a plain event table, one row per membership change:
| @timestamp | action | group | member | member_sid | administrator |
|---|---|---|---|---|---|
| 2026-07-29 14:02:11 | added | Helpdesk Tier 1 | CN=m.klein,OU=Users,DC=corp,DC=local | S-1-5-21-…-4821 | a.weber |
| 2026-07-29 03:47:52 | added | Domain Admins | S-1-5-21-…-1174 | svc_provision |
The second row is the kind of thing the rest of this list exists to isolate: an off-hours addition to a privileged group, made by a service account, with no resolvable member name.
2. Changes to Domain Admins specifically
The broad view is useful for investigation, but day-to-day you care much more about a short list of high-privilege groups than about every group in the domain. This query narrows the same event set down to Domain Admins, Enterprise Admins, Schema Admins, DnsAdmins, Group Policy Creator Owners, and a couple of infrastructure-specific groups (VMware Admins, vCenter Administrators), and adds a filter for changes made outside normal business hours.
The after-hours filter does much of the work here. A membership change to Domain Admins at 2 pm on a Tuesday might be entirely legitimate change management; the same change at 3 am is a different story. Adjust the hour range and the group list to match your own change-management windows and your own list of privileged groups — the ones above are just a starting point.
Set the timezone argument on formatTime() to your own zone, and do not skip it. Without it, LogScale formats the timestamp in UTC, so a 9-to-17 filter silently becomes 10-to-18 or 11-to-19 for anyone in Central Europe, depending on daylight saving time. The first and last hour of the working day then either escape the filter or get flagged as after-hours, which is the sort of error that only surfaces once someone questions an alert. The parseInt() step is there for the same reason: formatTime() returns a string like "03", and converting it explicitly keeps the comparison numeric instead of relying on LogScale to coerce it.
in(field="#windows.EventID", values=[4732,4728,4756,4729,4733,4757])
| case {
in(field="#windows.EventID", values=[4732,4728,4756]) | action:= "added";
in(field="#windows.EventID", values=[4733,4729,4757]) | action:= "removed";
}
// look for these groups
| in(field="windows.EventData.TargetUserName", values=["Domain Admins",
"Enterprise Admins",
"Schema Admins",
"DnsAdmins",
"Group Policy Creator Owners",
"VMware Admins",
"vCenter Administrators"])
// only show outside office hours, in local time
| hour := formatTime(format="%H", field=@timestamp, timezone="Europe/Berlin")
| parseInt(hour)
| hour < 9 or hour > 17
// format the table
| rename(field="windows.EventData.TargetUserName", as="group")
| rename(field="windows.EventData.MemberName", as="member")
| rename(field="windows.EventData.MemberSid", as="member_sid")
| rename(field="windows.EventData.SubjectUserName", as="administrator")
| table([@timestamp, action, group, member, member_sid, administrator])
| sort(@timestamp, order=desc)
Detect after-hours changes to privileged Active Directory groups on mylogscale.
3. Self-service additions: when an account adds itself to a group
Legitimate administrators rarely add their own accounts to a group — normal workflows involve managing other people’s access, not your own. When SubjectUserSid matches MemberSid in the event data, the same account both requested and received the change, which strongly indicates that something other than routine administration is going on.
In practice, this pattern shows up in a handful of scenarios: an attacker who has compromised a low-privilege account using a token or session with just enough rights to escalate itself, a misconfigured automation script running under a personal account instead of a dedicated service account, or — less alarmingly — an admin taking a shortcut during a rushed change. None of these should happen regularly, and none of them should happen silently. That’s what makes this one of the higher-signal, lower-noise queries in this list: the baseline rate of legitimate self-additions in most environments is close to zero, so any hit deserves a look.
in(field="#windows.EventID", values=[4732,4728,4756])
| test(windows.EventData.MemberSid == windows.EventData.SubjectUserSid)
// format the table
| rename(field="windows.EventData.TargetUserName", as="group")
| rename(field="windows.EventData.MemberName", as="member")
| rename(field="windows.EventData.MemberSid", as="member_sid")
| rename(field="windows.EventData.SubjectUserName", as="administrator")
| table([@timestamp, group, member, member_sid, administrator])
| sort(@timestamp, order=desc)
Detect Active Directory group self-additions on mylogscale.
4. Group membership changes performed by service accounts
Service accounts typically handle automation, not manual management. Provisioning tools, sync jobs, and identity platforms often use a service account to add or remove users from groups as part of their normal tasks. This is expected. What isn’t expected is a service account making changes outside its usual automation window. That usually means someone is using the service account’s credentials directly, instead of letting automation handle it.
This query filters for SubjectUserName values matching common service-account naming prefixes (svc_, sa_, srv_) — adjust the regex to match your own naming convention. The prefix filter on its own would surface every legitimate sync job as well, so it carries the same time-of-day filter as query 2, inverted: automation is expected overnight, and a change from the same account in the middle of the working day is the interesting case. If your sync job runs at 2 am, anything from that account at 2 pm is worth a second look. Set the hour range to the inverse of your own automation window, and use the same timezone you set in query 2.
in(field="#windows.EventID", values=[4732,4728,4756,4729,4733,4757])
| windows.EventData.SubjectUserName=/^(svc_|sa_|srv_)/i
// only show during office hours, when automation should be idle
| hour := formatTime(format="%H", field=@timestamp, timezone="Europe/Berlin")
| parseInt(hour)
| hour >= 9 and hour <= 17
// format the table
| rename(field="windows.EventData.TargetUserName", as="group")
| rename(field="windows.EventData.MemberName", as="member")
| rename(field="windows.EventData.MemberSid", as="member_sid")
| rename(field="windows.EventData.SubjectUserName", as="administrator")
| table([@timestamp, group, member, member_sid, administrator])
| sort(@timestamp, order=desc)
Detect Active Directory group changes by service accounts during office hours on mylogscale.
5. Daily rollup for a compliance report
Auditors checking access controls usually don’t want a raw event feed. They want a clear number to reference and a trend they can compare over time. This query reduces the events to a daily count for each group, making months of membership changes easy to show on a single slide or spreadsheet row.
It’s not a detection query in the same sense as the others; it won’t tell you that something suspicious happened, only how much membership activity a group saw on a given day. But it pairs well with the others — if the daily rollup for Domain Admins suddenly jumps from its usual near-zero baseline, that’s a good prompt to go back to query 2 and look at what actually changed. Keep the same timezone you used there, otherwise a late-evening change lands on the following day in the report and the two queries stop agreeing with each other.
in(field="#windows.EventID", values=[4732,4728,4756,4729,4733,4757])
| day := formatTime(format="%Y-%m-%d", field=@timestamp, timezone="Europe/Berlin")
| rename(field="windows.EventData.TargetUserName", as="group")
| groupBy([day, group], function=count())
| sort(day, order=desc)
Daily Active Directory group membership changes on mylogscale.
Which gives you exactly the shape an auditor asks for:
| day | group | _count |
|---|---|---|
| 2026-07-29 | Helpdesk Tier 1 | 14 |
| 2026-07-29 | Domain Admins | 2 |
| 2026-07-28 | Helpdesk Tier 1 | 9 |
Wiring these into alerts
Queries 3 and 4 are the best ones to set up as scheduled searches with a notifier. Both describe behaviour that should almost never happen in a well-managed environment. A self-addition, or a service account making changes during the hours it is supposed to be idle, is rare enough that a false positive only takes a couple of minutes to check, while a real issue is important to catch right away. Queries 1, 2, and 5 work better as saved dashboards you review regularly. Running them as alerts would mostly create noise, since group membership changes and daily activity can shift for many routine reasons.
For the lookback window, 5 to 15 minutes is a good place to start. However, check how your Windows event forwarding works before deciding. If events come in batches instead of a steady stream, a 5-minute window can leave gaps where an event falls outside both the previous and current search. It’s usually safer to make the window a bit wider and let the scheduled search handle deduplication, rather than trying to keep the interval as short as possible.
Make sure the notifier payload actually includes member, member_sid, group, and administrator rather than just a generic “match found” message — whoever receives the alert should be able to act on it without opening LogScale first. And if query 4 turns out to be noisier than expected in your environment (some shops have more interactive service-account usage than they’d like to admit), consider adding a minimum hit count or a short suppression window before the notifier fires, so a single automation blip doesn’t trigger an unnecessary alert.
All Active Directory queries
Browse all Active Directory queries on mylogscale for additional detection, investigation, and reporting use cases.