Q: Is there an easy way to detect and changes to important the membership of AD Groups?
A: Easy using PowerShell 7, WMI, and the CIM Cmdlets.
WMI
Windows Management Instrumentation (WMI) is an important component of the Windows operating system. WMI is an infrastructure of both management data and management operations on Windows-based computers. You can use PowerShell to retrieve information about your host, such as the BIOS Serial number. Additionally, you can perform management actions, such as creating an SMB share.
WMI is, in many cases, just another way to do things. For example, you can use WMI to create an SMB share by using the Create
method of the Win32_Share class. For more information, see the documentation page for the Win32_Share class. In most cases, you use PowerShell cmdlets, such as the SMB cmdlets, to manage your SMB shares. The value of WMI is that it can provide you access to more information and features that are not available using cmdlets.
In writing this article, I assume you have an understanding of WMI. In specific, I assume you understand WMI namespaces, classes, properties, and methods. If not, you might like to look at the WMI Documentation.
WMI Eventing
A cool and very powerful feature of WMI is eventing. With WMI eventing, you can subscribe to an event, such as the change of an AD group’s membership. If and when that event occurs, you can take some action, such as writing to a log file or sending an email. WMI event handling is fairly straightforward and very powerful – if you know what classes to use and how to use them!
There are two broad types of eventing within WMI. With temporary eventing, you use PowerShell inside a PowerShell session to subscribe to the events and process them. If you close that session, the event subscriptions and event handlers are lost. To enable temporary WMI event monitoring to continue, you must leave the host turned on and logged in, which may be a suboptimal situation. Temporary event handling can be great for troubleshooting but not ideal for longer-term monitoring.
With permanent event handling, you also tell WMI what events to do and what to do when they occur. To do that, you add the details of event handling to the WMI repository. By doing so, WMI can continue to monitor the event after close your session, logoff, or even reboot your host. And with PowerShell and PowerShell remoting, it is pretty easy to deploy WMI event detection on multiple servers.
I warn you that the documentation for eventing may not be great in all cases. Some documentation is focused on developers and thus lacks good PowerShell examples.
Permanent Event Consumers
Within WMI, a permanent event consumer is a built-in COM component that does something when any given event occurs. In theory, I suppose you could develop a private WMI event consumer, but I have never seen one developed. I am not suggesting that someone has not done it, of course. If you have seen this – please comment as I’d love to see the code and understand the details.
There are five key WMI permanent event consumers which Microsoft provides within Windows:
- Active Script Consumer: You use this to run a specific VBS script.
- Log File Consumer: This handler writes strings of customisable text to a text file.
- NT Event Log Consumer: This consumer writes event details into the Windows Application event log.
- SMTP Event Consumer: You use this consumer to send an SMTP email message when an event occurs
- Command Line Consumer: This consumer runs a program with parameters, for example, run PowerShell 7 and specific a script to run.
The Active Script consumer only runs VBS scripts. Short of redeveloping the COM component, you can not use this consumer with Powershell scripts. The Log File Consumer is excellent for writing short highly-customised messages to a text file but can take some time and effort to implement. For most IT Pros, the Command Line consumer is the one to choose. With this consumer, you get WMI to run a PowerShell script any time an event occurs, such as a change to an AD group. Let’s look at how you use this permanent event consumer to discover changes to the membership of the Enterprise Admins group.
Creating a permanent event handler
With WMI permanent event handling, you need to create three objects within the CIM database
- Event filter – the filter tells WMI which event to detect, such as a change in the change to an AD group.
- Event consumer – this tells WMI which permanent event consumer to run and how to invoke the consumer, such as to run the Command Line consumer and run
Monitor.ps1
. - Event binding – this binds the filter (what event to look out for) to the consumer (what to do when the event occurs happens)
To carry out these three operations, you inserting new objects into three specific WMI system classes. The WMI system class instances enable WMI to continue to process events after you stop your PowerShell session, log off, or restart your host.
In the code below, you use the Command Line consumer to detect changes to the AD’s Enterprise Admins group. Every time the change event occurs, you want WMI to run a specific script, namely Monitor.ps1
. This script displays a list of the current members of the Enterprise Admins group to a log file and reports whether the membership now contains unauthorized users. If the script finds that an unauthorized user is now a group member, it writes details to a text file for you to review later.
The Solution
There are several steps in this solution. So please, fasten your seat belts, and away we go.
Setting up
In this post, you want to detect whether an unauthorized user is a member of the Enterprise Admins group. You must first create a file of authorized users. Then you create two helper functions to assist you in testing the code. The function to delete all aspects of the WMI event filter from your host is useful unless you plan to keep the filter running forever.
# 1. Create a list of valid users for the Enterprise Admins group
$OKUsersFile = 'C:\Foo\OKUsers.Txt'
$OKUsers = @'
Administrator
JerryG
'@
$OKUsers |
Out-File -FilePath $OKUsersFile
# 2. Define two helper functions to get/remove permanent events
Function Get-WMIPE {
'*** Event Filters Defined ***'
Get-CimInstance -Namespace ROOT\subscription -ClassName __EventFilter |
Where-Object Name -eq "EventFilter1" |
Format-Table Name, Query
'***Consumer Defined ***'
$NS = 'ROOT\subscription'
$CN = 'CommandLineEventConsumer'
Get-CimInstance -Namespace $ns -ClassName $CN |
Where-Object {$_.name -eq "EventConsumer1"} |
Format-Table Name, CommandLineTemplate
'***Bindings Defined ***'
Get-CimInstance -Namespace ROOT\subscription -ClassName __FilterToConsumerBinding |
Where-Object -FilterScript {$_.Filter.Name -eq "EventFilter1"} |
Format-Table Filter, Consumer
}
Function Remove-WMIPE {
Get-CimInstance -Namespace ROOT\subscription __EventFilter |
Where-Object Name -eq "EventFilter1" |
Remove-CimInstance
Get-CimInstance -Namespace ROOT\subscription CommandLineEventConsumer |
Where-Object Name -eq 'EventConsumer1' |
Remove-CimInstance
Get-CimInstance -Namespace ROOTsubscription __FilterToConsumerBinding |
Where-Object -FilterScript {$_.Filter.Name -eq 'EventFilter1'} |
Remove-CimInstance
}
These two steps produce no output. When you create the OkUsers.txt
file – ensure the users in the file are actually in your AD.
Create a WQL event query and WMI event filter
To tell WMI what event you want WMI to detect, you create a WMI Query Language (WQL) query. In each WMI namespace, you can find various system classes representing event notification. You can use the __InstanceModificationEvent class, for example, to detect any modification of an instance (in that namespace). You can likewise use the __MethodInvocationEvent class to track WMI method invocations. If things change anywhere in a Windows host, you can probably use a WMI event to detect the change.
Here’s the code to create the WQL query and the WMI event filter
# 3. Create a WQL event filter query
$Group = 'Enterprise Admins'
$Query = @"
SELECT * From __InstanceModificationEvent Within 10
WHERE TargetInstance ISA 'ds_group' AND
TargetInstance.ds_name = '$Group'
"@
# 4. Create the event filter
$Param = @{
QueryLanguage = 'WQL'
Query = $Query
Name = 'EventFilter1'
EventNameSpace = 'ROOT/directory/LDAP'
}
$IHT = @{
ClassName = '__EventFilter'
Namespace = 'ROOT/subscription'
Property = $Param
}
$InstanceFilter = New-CimInstance @IHT
In this code (which produces no output), the filter query does not state which namespace the query is looking at, just that there is a target class for WMI to monitor. In the event filter, you create a new occurrence in the EventFilter class in the ROOT/Subscription namespace. This occurrence tells WMI to monitor the ROOT/directory/LDAP namespace for the ds_group class.
Creating the Event Consumer
The next step is to create an event consumer – what you want WMI to do when it detects the event has occurred. In our example, you want the WMI permanent event handler COM object to run a script Monitor.ps1
any time the event occurs. So whenever WMI detects a change to the Enterprise admins group, you want WMI to run the script.
# 5. Create Monitor.ps1
$MONITOR = @'
$LogFile = 'C:\Foo\Grouplog.Txt'
$Group = 'Enterprise Admins'
"On: [$(Get-Date)] Group [$Group] was changed" |
Out-File -Force $LogFile -Append -Encoding Ascii
$ADGM = Get-ADGroupMember -Identity $Group
# Display who's in the group
"Group Membership"
$ADGM | Format-Table Name, DistinguishedName |
Out-File -Force $LogFile -Append -Encoding Ascii
$OKUsers = Get-Content -Path C:\Foo\OKUsers.txt
# Look at who is not authorized
foreach ($User in $ADGM) {
if ($User.SamAccountName -notin $OKUsers) {
"Unauthorized user [$($User.SamAccountName)] added to $Group" |
Out-File -Force $LogFile -Append -Encoding Ascii
}
}
"**********************************`n`n" |
Out-File -Force $LogFile -Append -Encoding Ascii
'@
$MONITOR | Out-File -Path C:\Foo\Monitor.ps1
# 6. Create a WMI event consumer
# The consumer runs PowerShell 7 to execute C:\Foo\Monitor.ps1
$CLT = 'Pwsh.exe -File C:\Foo\Monitor.ps1'
$Param =[ordered] @{
Name = 'EventConsumer1'
CommandLineTemplate = $CLT
}
$ECHT = @{
Namespace = 'ROOT/subscription'
ClassName = "CommandLineEventConsumer"
Property = $param
}
$InstanceConsumer = New-CimInstance @ECHT
The monitoring script is fairly simple – each time the event occurs, it prints some information to a log file. Then it looks to see if the Enterprise Admins group contains unauthorized users – and if so, the script reports that fact to the log file. This script is fairly simple, and you can embellish. as needed. You could, for example, remove all unauthorized users.
To create a WMI event consumer, you add a new occurrence to the CommandLineEventConsumer class within the namespace ROOT/Subscription.
Binding the Event Filter and the Event Consumer
With the event filter and event consumer details added to WMI, you need to bind the two – telling WMI to detect THAT event and when it occurs, run THIS script. You could pre-create, for example, multiple event filters and event consumers. Once the binding is in place, WMI starts the monitoring process.
# 7. Bind the filter and consumer
$Param = @{
Filter = [ref]$InstanceFilter
Consumer = [ref]$InstanceConsumer
}
$IBHT = @{
Namespace = 'ROOT/subscription'
ClassName = '__FilterToConsumerBinding'
Property = $Param
}
$InstanceBinding = New-CimInstance @IBHT
Checking your work
A great way to check your work is to call the Get-WMIPE
function you created earlier. What you should see is:
PS > # 8. Viewing the event registration details
PS > Get-WMIPE
*** Event Filters Defined ***
Name Query
---- -----
EventFilter1 SELECT * From __InstanceModificationEvent Within 10
WHERE TargetInstance ISA 'ds_group' AND
TargetInstance.ds_name = 'Enterprise Admins'
***Consumer Defined ***
Name CommandLineTemplate
---- -------------------
EventConsumer1 Pwsh.exe -File C:\Foo\Monitor.ps1
***Bindings Defined ***
Filter Consumer
------ --------
__EventFilter (Name = "EventFilter1") CommandLineEventConsumer (Name = "EventConsumer1")
Testing your work
So having created the event query, the event filter, the event consumer and the filter to consumer binding, you can test your work. The easiest way to test this is to add a new user to the group. Then, wait a few seconds for WMI to process the event, then look at the output. If everything is working correctly, you should see this output:
PS > # 9. Adding a user to the Enterprise Admins group
PS > Add-ADGroupMember -Identity 'Enterprise admins' -Members Malcolm
PS >
PS > # 10. Viewing the Grouplog.txt file
PS > Get-Content -Path C:\Foo\Grouplog.txt
On: [04/20/2021 15:41:49] Group [Enterprise Admins] was changed
Name DistinguishedName
---- -----------------
Malcolm CN=Malcolm,OU=IT,DC=Reskit,DC=Org
Jerry Garcia CN=Jerry Garcia,OU=IT,DC=Reskit,DC=Org
Administrator CN=Administrator,CN=Users,DC=Reskit,DC=Org
Unauthorized user [Malcolm] added to Enterprise Admins
**********************************
Troubleshooting
This code, of course should “just work”. If not, you need to perform troubleshooting and here are three things to look for:
- Is the WQL query correct?
- Are the event and subscription classes in the namespace(s) you think they are in?
- Is the
Monitor.ps1
script doing what you actually wanted?
The Microsoft-Windows-WMI-Activity/Operational event log can be useful in tracking down issues. And if you get stuck, feel free to visit the Spiceworks PowerShell forum.
Tidying up
After you play with a WMI filter like this, make sure you clean up. You probably don’t want the filter to run forever, so remove it as soon as you can. To remove it, invoke the Remove-WMIPE
function. And you should probably remove any inappropriate users from the Enterprise Admins group
# 11. Tidying up
Remove-WMIPE # invoke this function you defined above
$RGMHT = @{
Identity = 'Enterprise Admins'
Member = 'Malcolm'
Confirm = $false
}
Remove-ADGroupMember @RGMHT
This step creates no output. You might wish to call Get-WMIPE
again to verify you have removed all three class occurrences.
Summary
WMI eventing is very powerful and straightforward to implement. There are thousands of WMI events you could subscribe to and which may help troubleshooting activities. In this case, you are examining unauthorized changers to an AD group. The WMI documentation does not provide a definitive guide to the events you might be interested in – at least that I can find.
For some more details on using WMI in PowerShell 7, see my recently published PowerShell 7 book. I devote chapter 9 to WMI and using the CIM cmdlets. You can find the scripts from this blog post and that chapter in my GitHub repository.
This is a really interesting way of creating an alert. I did this years ago with SCCM 2012 and we were having lots of issues with the SQL WMI providers disappearing.
That being said, I looking at the root\directory\LDAP classes on a member server and a domain controller, this would have to be configured on a DC as that's the only place where the ds_name class exists.
I tried this, and the set up looks good,...
Thanks for the comment.
There is no ds_name class on either a member server or a DC. The reference in the code is to targetinstance.ds_name, that is the ds_name property of the ds_group that has been changed. And that property exists on a member server.
As to why the script never kicks off - take a look at the WMI event log. It should tell you of any errors with the script.
Using WMI Explorer or get-wmiobject, on a DC I can query the ROOT\directory\LDAP namespace and find the ds_group class on a DC, which doesn't exist on a member server.
This is the event I'm seeing in the event log, but the monitor script doesn't seem to kick off and update the log file. If I run the script manually, it updates the log file correctly. Wasn't sure if the query having the \n new...
Not sure where the \n came from, but that could be a cause. ALthough I did not find that an issue here.
Also, have you bound the consumer to the filter?
Try the function to display the bindings – what does that show?
I tried it again, cleaned up the query so it was all on 1 line, no go. I see the single WMI log entry, but nothing about the script being triggered. Should there be another event about the powershell script running?
Everything looks good for the bind.
*** Event Filters Defined ***
Name Query
---- -----
EventFilter1 SELECT...