Last week, Adam Bertram sent out a tweet looking for any PowerShell code to notify the user when a new Active Directory user account had been created. I dug out some very, very old code that used a WMI event subscription to watch Active Directory for such an event. The code I shared was something I put together back in the PowerShell v2 days, but it still worked today. However, it used Register-WMIEvent which in today's PowerShell world I consider deprecated. We should be using the newer CIM equivalent cmdlets. I decided to take a little time and polish up the code and bring it up to (my) current standards.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
WMI/CIM Event Subscriptions
In the Windows world, when something happens like a service starting or a process ending, we get an event. In Active Directory, creating a new user account also fires an event. In many cases, you can watch for these events using WMI/CIM. You accomplish this by creating a temporary event subscription. This is like any other subscription. You are saying, "Send me a new issue when there is something new to report." You could get a new "issue" every 2 weeks or every 2 minutes.
You will use Register-CimIndicationEvent to create the subscription. You can create subscriptions for events on your local computer or remote. This subscription only lasts for the duration of your PowerShell session. But a word of caution, I would be careful about building a complete management system around it except for small environments. While it is nice to know when something happens like a service stopping or a user account being modified, in larger environments, you are better off investing in tools and software designed for that type of monitoring. I would use temporary event subscriptions for short-term monitoring or troubleshooting. With this in mind, consider the code in this article as proof-of-concept.
Register-CIMIndicationEvent
I am trusting you will take the time to read full help and examples for this cmdlet. WMI events is a complicated topic and I'm not going to get too deep. There are several ways to use the cmdlet. I'm going to use an old-fashioned query.
Register-CimIndicationEvent -Query "Select * from __InstanceCreationEvent Within 10 where TargetInstance ISA 'DS_USER'" -Namespace root\directory\ldap -SourceIdentifier NewUser
I am running this code on a domain member Windows 10 desktop. The query says, "check for a creation event every 10 seconds where the object type is a user account." The Within portion is referred to as polling. You don't want to poll too quickly. If you truly need to know within seconds when an event happens, you need to find a true software solution. I'm using 10 seconds for my demo.
This will create an event subscription.
The SourceIdentifier is how you can reference the subscriber and subsequent events. If you don't specify one, you'll get a GUID. Personally, I find defining my own identifiers much easier.
The event subscriber is running in my local PowerShell session, checking every 10 seconds for a new user creation event in the domain. I'll create a new domain user. 10 seconds later nothing has happened in my session. We'll change that later. Instead I'll run Get-Event.
Information about the user is buried in the event.
I'll come back to this object in a bit.
Register-ADWatcher
With these basics in mind, I wrote a PowerShell function that wraps around Register-CimIndicationEvent. The function allows me to watch for different types of events for different categories of Active Directory objects.
Function Register-ADWatcher {
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory, HelpMessage = "What type of object do you want to watch?")]
[ValidateSet("User", "Group", "Computer", "OU")]
[string]$Category,
[Parameter(Mandatory, HelpMessage = "What type of activity watch?")]
[ValidateSet("Create", "Modify", "Remove")]
[string]$Activity,
[Parameter(HelpMessage = "How often do you want to poll? Enter a value in seconds. The minimum and default is 10.")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ $_ -ge 10 })]
[int]$Poll = 10,
[Parameter(HelpMessage = "Specify an action scriptblock when the event fires.")]
[scriptblock]$Action,
[Parameter(HelpMessage = "Specify additional data to associate with the watcher.")]
[string]$MessageData,
[Parameter(HelpMessage = "Specify a name for the watcher")]
[string]$SourceIdentifier,
[parameter(HelpMessage = "Specify the name of a computer where you want to create the watcher.")]
[string]$Computername,
[switch]$Passthru
)
Write-Verbose "Starting $($myinvocation.mycommand)"
#remove bound parameters that don't belong to Register-CimIndicationEvent
$drop = "Category", "Activity", "Poll", "WhatIf", "Confirm","Passthru"
foreach ($item in $drop) {
if ($PSBoundParameters.ContainsKey($item)) {
[void]$PSBoundParameters.Remove($item)
}
}
Switch ($Category) {
"User" { $DS = "DS_USER" }
"Group" { $DS = "DS_GROUP" }
"Computer" { $DS = "DS_COMPUTER" }
"OU" { $DS = "DS_ORGANIZATIONALUNIT" }
}
Switch ($Activity) {
"Create" { $DSAction = "__InstanceCreationEvent" }
"Modify" { $DSAction = "__InstanceModificationEvent" }
"Remove" { $DSAction = "__InstanceDeletionEvent" }
}
$query = "Select * from $DSAction Within $Poll where TargetInstance ISA '$DS'"
$PSBoundParameters.Add("Query", $query)
$PSBoundParameters.Add("Namespace", "root\directory\ldap")
Write-Verbose "Creating an AD Watcher with these parameters"
$PSBoundParameters | Out-String | Write-Verbose
$target = "$Activity $Category within $Poll seconds"
If ($pscmdlet.ShouldProcess($target.toUpper(), "Register AD Watcher")) {
[void](Register-CimIndicationEvent @PSBoundParameters)
if ($Passthru) {
Start-sleep -Seconds 2
Get-EventSubscriber | Sort-Object -property SubscriberID | Select-Object -last 1
}
}
Write-Verbose "Ending $($myinvocation.mycommand)"
}
I'll use this function to watch for new groups.
Register-ADWatcher -Category Group -Activity Create -MessageData "A new group has been created" -SourceIdentifier "NewGroup"
I'm eventually going to replace the new user subscription so I'll delete the current one.
Get-EventSubscriber newuser | Unregister-Event
Now to create a new group and see the event.
Helper Functions
I know information about the group is in there, but I'm too lazy to parse the object. Instead I'll write a helper function.
Function Get-TargetInstance {
[cmdletbinding()]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[object]$EventResult,
[Parameter(HelpMessage = "Only show defined properties")]
[switch]$ShowDefined
)
Process {
if ($ShowDefined) {
$t = $EventResult.SourceEventArgs.NewEvent.TargetInstance
$t.psobject.Properties.where({ $_.name -match "^DS|ADSIPath" -AND $_.value }) |
ForEach-Object -Begin {
#create a temporary ordered hashtable
$h = [ordered]@{
TimeGenerated = $EventResult.timeGenerated
EventClass = $EventResult.SourceEventArgs.NewEvent.CimSystemProperties.Classname
}
} -Process {
#add each property that has a value to the hashtable
$h.add($_.name, $_.value) } -End {
#Create a custom object from the hashtable
New-Object -TypeName PSObject -Property $h
}
}
else {
$EventResult.sourceEventArgs.NewEvent.TargetInstance
}
}
}
Because I know the object will have many undefined or null property values, I added a parameter to only show populated properties.
Actions
In looking at this, I can see that I fat-fingered the group name. Perfect. Let's create a watcher for changes to group objects. I can also show you how to add an Action. This is a scriptblock that runs when the event fires. I'm going to use the BurntToast module, which you can install from the PowerShell Gallery, to show me a notification.
Register-ADWatcher -Category Group -Activity Modify -SourceIdentifier "GroupChange" -Action { New-BurntToastNotification -Text "A group has changed"}
But there is a trade-off when using an action.
There is no event object. Well, I should say, there's no object written to my console. There is an object I can reference in my Action scriptblock. But first let me show what that is.
Here's a watcher for changed user objects.
Register-ADWatcher -Category User -Activity Modify -SourceIdentifier UserChange -Passthru
When a change event fires, you get a "before" and "after" object. Naturally you would like to know what changed. I tried using Compare-Object which would seem like the appropriate tool. But unless I specify a property, it doesn't give me a result. So I wrote my own comparison function.
Function Compare-DSObject {
#get-event -sourceIdentifier changeduser | Compare-DSObject
[cmdletbinding()]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[object]$EventResult
)
Process {
#write-host "Processing an event" -ForegroundColor Green
$p = $EventResult.SourceEventArgs.NewEvent.PreviousInstance
$t = $EventResult.SourceEventArgs.NewEvent.TargetInstance
$prop = $p.psobject.Properties.name.where( { $_ -match "^DS" })
Write-Verbose "Processing Changes to $($t.ADSIPath)"
foreach ($item in $prop) {
if (Compare-Object -ReferenceObject $p -DifferenceObject $t -Property $item) {
[pscustomobject]@{
PSTypeName = "DSComparison"
ADSIPath = $t.ADSIPath
TimeGenerated = $eventResult.TimeGenerated
Property = $item
PreviousValue = $p.$item
NewValue = $t.$item
}
}
}
} #process
}
As you can see, it really depends on what you want to do or see when an event fires.
In the Action scriptblock you can reference the event object with the built-in $event object. Here's a watcher for new user accounts that will use BurntToast and display the distinguished name of the new object.
$action = {
$toast = @{
Text = "$($event.MessageData) $($event.SourceEventArgs.NewEvent.TargetInstance.DS_distinguishedName)"
Sound = "Alarm5"
}
New-BurntToastNotification @toast
}
$watch = @{
Category = "User"
Activity = "Create"
MessageData = "A domain user account has been created."
SourceIdentifier = "NewUser"
Action = $Action
}
Register-ADWatcher @watch
The value of MessageData will be used in the notification.
Putting It All Together
Let me wrap up this article by pulling everything together. Let's look at a deletion event. I'd like to keep an audit trail in my small domain. When a user is deleted, I want to save the event information to disk and get my toast notification. In my action scriptblock, I'll export the event to disk using Export-CliXML.
$action = {
#export event to disk
if (-not (Test-Path C:\ADWatch)) {
New-Item -Path C:\ -Name ADWatch -ItemType Directory
}
$file = "{0}_{1}.xml" -f $event.sourceIdentifier.Replace(" ",""),($event.TimeGenerated.tostring("u").replace(" ","-").replace(":",""))
$export = Join-Path -Path C:\ADWatch -ChildPath $file
$event | Export-Clixml -Path $export
$ti = $event.SourceEventArgs.NewEvent.TargetInstance
$msg = "[$($event.TimeGenerated)] $($event.messagedata) $($ti.DS_DistinguishedName) See $export for details."
$toast = @{
Header = New-BTHeader -Title "AD Watcher"
Text = $msg
SnoozeAndDismiss = $True
AppLogo = "C:\scripts\db.png"
Sound = "alarm10"
}
New-BurntToastNotification @toast
}
Register-ADWatcher -Category User -Activity Remove -Action $action -SourceIdentifier "DeleteUser" -MessageData "ALERT! A domain user account has been removed." -Verbose
The target instance of the deleted user will be serialized to an XML file in C:\ADWatch. I'm creating a UTC time stamp as part of the file name.
The alert message indicates the file which is easily brought back in to PowerShell.
I can use my helper function to view the deleted user information.
Summary
As you have seen, it is possible to get notified when things happen in Active Directory. But just because you can do it, doesn't mean should. There are plenty of other options that are better choices, especially the closer to real-time notification you require. I'd say it also depends on what you need to do with the information. Do you just need to know or does something have to happen?
And to head off a question I'm sure to get, I have no idea if or how this would work with Azure AD. Someone with an Azure AD environment will have to let me know.
But for local events, whether in Active Directory or elsewhere, using CIM events can be a useful tool in your PowerShell toolbox.
great write-up! I am going to kick the tires on this for some events I might be interested in possibly watching!
Awesome result!
On the toast front if you make your $msg an array of strings they’ll show on their own lines. I think (need to confirm and document) you can have up to 5 lines but it’s best imho with 3.
Thanks for that tidbit. I was focusing on “quick and dirty” toast notifications.