PowerShell and Events: Permanent WMI Event Subscriptions

Wrapping up my series on PowerShell and Events, I will be talking about Permanent WMI Event Subscriptions and creating these using PowerShell.

Mentioned in my previous article on temporary events, WMI events are a very powerful and useful way to monitor for a wide variety of things with the only downside of those events being stopped when a console session is closed, a system is restarted or some other issue occurs that affects WMI. Enter the permanent WMI events. Unlike the temporary event, the permanent event is persistent object that will last through a reboot and continue to operate until it has been removed from the WMI repository.

There are multiple ways of setting up the WMI events and I will be covering 3 of those ways (as they deal with PowerShell) in this article. But before we dive into building out the event subscriptions, we first need to look at the basics behind building the events.

You need to be running as an Administrator on the system in order to create the event instances.

There are 3 pieces to the puzzle that have to be put together in order to have a fully functioning event subscription.  They are the Filter, Consumer and Binding.

Filter

The filter is a WQL query which says what you are looking for. Think back to my previous article on temporary events and how you setup a query that would be used to fire the action block. This is the exact same thing! You define the query and create the Filter WMI object.

Consumer

This would be your “Action” block that is in Register-WMIEvent with the exception of there being 5 possible Consumer classes to use with the Filter. Depending on your objective, you will use 1 of these ( or multiple that are binding by the same Filter) to perform an action of some sort. What are these 5 Consumer classes? Let’s take a look and see!

Consumer

Description

ActiveScriptEventConsumer

Executes a predefined script in an arbitrary scripting language when an event is delivered to it. This consumer is available on Windows 2000 and above.

CommandLineEventConsumer

Launches an arbitrary process in the local system context when an event is delivered to it. This consumer is available on Windows XP and above.

LogFileEventConsumer

Writes customized strings to a text log file when events are delivered to it. This consumer is available on Windows XP and above.

NTEventLogEventConsumer

Logs a specific message to the Windows NT event log when an event is delivered to it. This consumer is available on Windows XP and above.

SMTPEventConsumer

Sends an email message using SMTP every time that an event is delivered to it. This consumer is available on Windows 2000 and above.

Binding

The Binding is literally the glue that holds the Filter and Consumer together. Once you bind both of these together, you are enabling the WMI event subscriber to work right off the bat. To disable an existing WMI subscription, all you have to do is delete the binding instance and you will no longer have the subscription enabled.

 

Finding the WMI Instances

First off, lets see what I already have for Filters, Consumers and Bindings. We can use Get-WMIObject with the –Class parameter consisting of root\Subscription and then specifying the appropriate class that we wish to view.

#List Event Filters
Get-WMIObject -Namespace root\Subscription -Class __EventFilter

image

You can tell what kind of query is being used by the Query property of the Filter instance.

#List Event Consumers
Get-WMIObject -Namespace root\Subscription -Class __EventConsumer

image

#List Event Bindings
Get-WMIObject -Namespace root\Subscription -Class __FilterToConsumerBinding

Here you can see that the NTLogEventConsumer is being used by the __CLASS property and what exactly is being used in the other properties.

image

You can see from the __PATH property which Filter and Consumer is being used for a WMI event.

Option #1: Using [wmiclass]

The first method of creating the WMI event subscription is by taking advantage of the wmiclass type accelerator and using the CreateInstance() method. First I will start off by creating the instance of the Filter.

#Creating a new event filter
$instanceFilter = ([wmiclass]"\\.\root\subscription:__EventFilter").CreateInstance()

What I have available is an object that I can modify based on my requirements.

image

There really isn’t a lot to the Filter instance and all I really need to and  add is the Query, QueryLanguage, EventNamespace and a name that makes sense.

$instanceFilter.QueryLanguage = "WQL"
$instanceFilter.Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
$instanceFilter.Name = "ServiceFilter"
$instanceFilter.EventNamespace = 'root\cimv2'

I am confident that this is all I need to put in for the Filter instance and need to actually save this instance into the WMI repository. I do this by using the Put() method. Note that you cannot actually see this member by using Get-Member by itself. You have to use the –View property and specify All in order to see the Put method as well as others.

$instancefilter | Get-Member -View All

image

As I was saying, I will use the Put method to save this instance. When I save this, it will output an object that I will need to hold onto so I can use it’s Path property later on for the Binding.

$result = $instanceFilter.Put()
$newFilter = $result.Path

image

Up next is the Consumer. I am going to take the same approach as the Filter and first create the instance.

#Creating a new event consumer
$instanceConsumer = ([wmiclass]"\\.\root\subscription:LogFileEventConsumer").CreateInstance()

Next, I configure the object to create a log file whenever the Filter has fired.

$instanceConsumer.Name = 'ServiceConsumer'
$instanceConsumer.Filename = "C:\Scripts\Log.log"
$instanceConsumer.Text = 'A change has occurred on the service: %TargetInstance.DisplayName%'

Make sure that you have a valid path for the logfile, otherwise this will not work out so well. The %TargetInstance.Name% represents the name of the service that is changing, whether it is the state or something else.

Lastly, we need to save the object into the WMI repository.

$result = $instanceConsumer.Put()
$newConsumer = $result.Path

Same as what I did with the Filter, I am saving the path so it can be used with the binding.

Speaking of binding, that is up next to create.

#Bind filter and consumer
$instanceBinding = ([wmiclass]"\\.\root\subscription:__FilterToConsumerBinding").CreateInstance()

Now we bind the Filter and the Consumer together and save the instance.

$instanceBinding.Filter = $newFilter
$instanceBinding.Consumer = $newConsumer
$result = $instanceBinding.Put()
$newBinding = $result.Path

You may have noticed that I saved the Binding path. This will be used to remove the binding instance later on.

I will now stop a service and check the Scripts folder for a logfile.

Stop-Service wuauserv -Verbose

image

image

Ok, now I need to remove these before the next example. The following is 1 of the 2 examples that I will show in this article and really benefits from Option 1 more than any other.

##Removing WMI Subscriptions using [wmi] and Delete() Method
([wmi]$newFilter).Delete()
([wmi]$newConsumer).Delete()
([wmi]$newBinding).Delete()

By giving the path of the instance, the [wmi] type accelerator will cast it out to the proper type. This also means that you have access to the Delete() method and can easily remove all of the subscription instances without any issues.

Option #2: Using Set-WMIInstance

Up next is the second approach to creating permanent WMI using the Set-WMIInstance cmdlet. This method makes use of the –Arguments parameter which accepts a hashtable that will be used to define each instance and its properties. This method also lends itself very nicely to “splatting”.

I plan re-creating the same type of Filter and Consumer as Option 1 to keep everything simple and show the same results.

First off I am going to create the hash table that will be used with my splatting and these are also the common parameters which will not be changed with each WMI instance.

#Set up some hash tables for splatting
$wmiParams = @{
    Computername = $env:COMPUTERNAME
    ErrorAction = 'Stop'
    NameSpace = 'root\subscription'
}

Next up is the Filter creation.

#Creating a new event filter
$wmiParams.Class = '__EventFilter'
$wmiParams.Arguments = @{
    Name = 'ServiceFilter'
    EventNamespace = 'root\CIMV2'
    QueryLanguage = 'WQL'
    Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
}
$filterResult = Set-WmiInstance @wmiParams

Instead of saving a path, I am going to save the actual object which will be used for the Binding at the end of this section. You will also notice that I have a hash table within my splatting hash table to handle all of the arguments that will be applied to the creation of the instance.

Moving on to the Consumer creation:

$wmiParams.Class = 'LogFileEventConsumer'
$wmiParams.Arguments = @{
    Name = 'ServiceConsumer'
    Text = 'A change has occurred on the service: %TargetInstance.DisplayName%'
    FileName = "C:\Scripts\Log.log"
}
$consumerResult = Set-WmiInstance @wmiParams

The only things that really changed with my WMI hash table is the Class and Arguments which is easily overwritten by adding new things.

Last is the Binding creation to enable this event subscription.

$wmiParams.Class = '__FilterToConsumerBinding'
$wmiParams.Arguments = @{
    Filter = $filterResult
    Consumer = $consumerResult
}
$bindingResult = Set-WmiInstance @wmiParams

Using the Filter and Consumer object this time, I can create the Binding with no issue at all. You can run the same test as Option 1 and you will see the log file being written to.

The other removal option that I will show is using Remove-WMIObject. Using Get-WMIObject to locate the instance and piping into Remove-WMIObject, you can easily remove one or all of the created WMI instances.  Depending on your objective, you can either Filter for each instance using –Filter or just go crazy and remove everything!

##Removing WMI Subscriptions using Remove-WMIObject
#Filter
Get-WMIObject -Namespace root\Subscription -Class __EventFilter -Filter "Name='ServiceFilter'" | 
    Remove-WmiObject -Verbose
 
#Consumer
Get-WMIObject -Namespace root\Subscription -Class LogFileEventConsumer -Filter "Name='ServiceConsumer'" | 
    Remove-WmiObject -Verbose
 
#Binding
Get-WMIObject -Namespace root\Subscription -Class __FilterToConsumerBinding -Filter "__Path LIKE '%ServiceFilter%'"  | 
    Remove-WmiObject -Verbose

 

image

My previously created instances are now no longer!

Option #3 Using the PowerEvents Module

Wrapping up this article, I wanted to throw out an excellent module called PowerEvents (https://powerevents.codeplex.com/) that Trevor Sullivan (Twitter | Blog) has created. Not only does it come with some great examples, but it is incredibly easy to use! Let’s repeat the same type of event subscription that we have done before in the previous options. If you have the option to download and use this, I would recommend it!

First off, since this is a Module (and assuming you are on V2), it will need to be imported first.

#Import the module, if not running PowerShell V3 or above
Import-Module PowerEvents

image

We only have 3 commands to work with, but that is all we need to get this done.

image

First I set up some hash tables to splat against the New-WMIEventFilter and New-WMIEventConsumer functions.

#Set up a hash table for parameters
$filterParams = @{
    Computername = $env:COMPUTERNAME
    QueryLanguage = 'WQL'
    Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
    EventNamespace = 'root\CIMV2'
    Namespace = 'root\subscription'
    Name = 'ServiceFilter'
}
$consumerParams = @{
    Computername = $env:COMPUTERNAME
    ConsumerType = 'LogFile'
    Namespace = 'root\subscription'
    FileName = "C:\Scripts\Log.log"
    Name = 'ServiceConsumer'
    Text = 'A change has occurred on the service: %TargetInstance.DisplayName%'
}

Now we can create the Filter and Consumer, keeping in mind that we need to save the output object so it can be used with New-WMIFilterToConsumerBinding function.

#Create the Filter and Consumer
$filterResult = New-WmiEventFilter @filterParams
$consumerResult = New-WmiEventConsumer @consumerParams

Last on this is to bind the Filter and Consumer together.

$bindingParams = @{
    Computername = $env:COMPUTERNAME
    Filter = $filterResult
    Consumer = $consumerResult
}
 
#Create the binding
$bindingResult = New-WmiFilterToConsumerBinding @bindingParams

Deciding whether or not to save the output of the binding creation is up to you. You could just as easily save it to $Null or even let the object be displayed. Now we have an enabled event subscription in which you will see updates to the logfile as mentioned previously. How to remove these instances is up to you. You can use either of my methods listed in Options 1 and 2 or you can go about it another way.

While I repeated the same type of monitoring with each example, this should at least get you started in the right direction in creating your own types of permanent WMI events!

With that, this wraps up my series on PowerShell and Events with Permanent WMI Events.  Hopefully this has shown some various approaches to monitoring using PowerShell.

Posted in powershell | Tagged , , , , , | 10 Comments

Get All Members of a Local Group Using PowerShell

I wrote a function a while back that is used to query a local group on a remote or local system (or systems) and based on the –Depth parameter, will perform a recursive query for all members of that group to include local and domain groups and users. I felt that it was something worth sharing out just in case someone has a need for it.  It also isn’t the same script that I wrote back in January here: https://learn-powershell.net/2013/01/22/find-and-report-members-of-a-local-group/

To avoid an issue with circular groups (probably not the technical name for it), I use a hash table to manage the local/domain groups to ensure that they are not queried again later on. By circular groups, I am talking about groups that are members of a parent group that may have the parent group listed as a member later on down the group membership. A more understandable example is here:

image

Because Administrators exist in Group3 and Administrators has Group3 as a member, this madness will never stop or will stop when it hits some limit on depth. Regardless, this would be a very annoying thing to have happen.

    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias('CN','__Server','Computer','IPAddress')]
        [string[]]$Computername = $env:COMPUTERNAME,
        [parameter()]
        [string]$Group = "Administrators",
        [parameter()]
        [int]$Depth = ([int]::MaxValue)
    )

Nothing really new here. I am setting up my parameters for Computername, a Group name (which defaults to’Administrators and the Depth limit. You will notice that I am using [int]::MaxValue which is 2147483647 which is my ”unlimited” recursion. If someone has a nested group that far down, then my hats off to you!

#region Extra Configurations
Write-Verbose ("Depth: {0}" -f $Depth)
#endregion Extra Configurations
#region Helper Functions
Function Get-NetBIOSDomain {
    Try {
        $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        $Root = $Domain.GetDirectoryEntry()
        $Base = ($Root.distinguishedName)

        # Use the NameTranslate object.
        $Script:Translate = New-Object -comObject "NameTranslate"
        $Script:objNT = $Translate.GetType()

        # Initialize NameTranslate by locating the Global Catalog.
        $objNT.InvokeMember("Init", "InvokeMethod", $Null, $Translate, (3, $Null))

        # Retrieve NetBIOS name of the current domain.
        $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (1, "$Base"))
        $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 3)  
    } Catch {Write-Warning ("{0}" -f $_.Exception.Message)} 
}

This piece of code starts out with some verbose output (if using –Verbose) that will show the parameters being used as well as what the current depth that will be allowed for the nested groups.

The Get-NetBIOSDomain function is used only to help get the distinguishedName of a domain group in the Get-DomainGroupMember function (shown later in this article).

Function Get-LocalGroupMember {
    [cmdletbinding()]
    Param (
        [parameter()]
        [System.DirectoryServices.DirectoryEntry]$LocalGroup
    )
    $Counter++
    # Invoke the Members method and convert to an array of member objects.
    $Members= @($LocalGroup.psbase.Invoke("Members"))

    ForEach ($Member In $Members) {                
        Try {
            $Name = $Member.GetType().InvokeMember("Name", 'GetProperty', $Null, $Member, $Null)
            $Path = $Member.GetType().InvokeMember("ADsPath", 'GetProperty', $Null, $Member, $Null)
            # Check if this member is a group.
            $isGroup = ($Member.GetType().InvokeMember("Class", 'GetProperty', $Null, $Member, $Null) -eq "group")
            If (($Path -like "*/$Computer/*")) {
                $Type = 'Local'
            } Else {$Type = 'Domain'}
            New-Object PSObject -Property @{
                Computername = $Computer
                Name = $Name
                Type = $Type
                ParentGroup = $LocalGroup.Name[0]
                isGroup = $isGroup
                Depth = $Counter
            }
            If ($isGroup) {
                Write-Verbose ("{0} is a group" -f $Name)
                # Check if this group is local or domain.
                Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth)
                If ($Counter -lt $Depth) {
                    If ($Type -eq 'Local') {
                        If ($Groups[$Name] -notcontains 'Local') {
                            Write-Verbose ("{0}: Getting local group members" -f $Name)
                            $Groups[$Name] += ,'Local'
                            # Enumerate members of local group.
                            Get-LocalGroupMember $Member
                        }
                    } Else {
                        If ($Groups[$Name] -notcontains 'Domain') {
                            Write-Verbose ("{0}: Getting domain group members" -f $Name)
                            $Groups[$Name] += ,'Domain'
                            # Enumerate members of domain group.
                            Get-DomainGroupMember $Member $Name $True
                        }
                    }
                }
            }
        } Catch {
            Write-Warning ("{0}" -f $_.Exception.Message)
        }
    }
}

 

Get-LocalGroupMember is my first helper function when it comes to getting members of a group. In this case, I am gathering the members of a local group. A number of possibilities exist here such as whether the member is actually a User, or if it is another group that is either local on the system or a domain group. Based on the group type, it will either call itself again for the local group or call the Get-DomainGroupMember function which will be explained next. You can tell from my $Groups variable that it is the hash table used to make sure that the group hasn’t already queried as well as the counter which helps to determine the depth level of the current group and its members.

Function Get-DomainGroupMember {
    [cmdletbinding()]
    Param (
        [parameter()]
        $DomainGroup, 
        [parameter()]
        [string]$NTName, 
        [parameter()]
        [string]$blnNT
    )
    $Counter++
    If ($blnNT -eq $True) {
        # Convert NetBIOS domain name of group to Distinguished Name.
        $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (3, ("{0}{1}" -f $NetBIOSDomain.Trim(),$NTName)))
        $DN = $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 1)
        $ADGroup = [ADSI]"LDAP://$DN"
    } Else {
        $DN = $DomainGroup.distinguishedName
        $ADGroup = $DomainGroup
    }            
    ForEach ($MemberDN In $ADGroup.Member) {
        $MemberGroup = [ADSI]("LDAP://{0}" -f ($MemberDN -replace '/','\/'))
        New-Object PSObject -Property @{
            Computername = $Computer
            Name = $MemberGroup.name[0]
            Type = 'Domain'
            ParentGroup = $NTName
            isGroup = ($MemberGroup.Class -eq "group")
            Depth = $Counter
        }
        # Check if this member is a group.
        If ($MemberGroup.Class -eq "group") {  
            Write-Verbose ("{0} is a group" -f $MemberGroup.name[0])  
            Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth)               
            If ($Counter -lt $Depth) {
                If ($Groups[$MemberGroup.name[0]] -notcontains 'Domain') {
                    Write-Verbose ("{0}: Getting domain group members" -f $MemberGroup.name[0])
                    $Groups[$MemberGroup.name[0]] += ,'Domain'
                    # Enumerate members of domain group.
                    Get-DomainGroupMember $MemberGroup $MemberGroup.Name[0] $False
                }                                                
            }
        }
    }
}

The Get-DomainGroupMember is my second helper function used to get group members. As the name implies, this will gather the group memberships that have been queried. the NetBIOSDomain name is also used here to find out the actual distinguishedName of the group so I can be used with the [ADSI] accelerator to make the query for group members. As with my Get-LocalGroupMember function, this makes use of the same hash table and counter to handle circular groups and recursion depth.

    Process {
        #region Get Local Group Members
        ForEach ($Computer in $Computername) {
            $Script:Groups = @{}
            $Script:Counter=0
            # Bind to the group object with the WinNT provider.
            $ADSIGroup = [ADSI]"WinNT://$Computer/$Group,group"
            Write-Verbose ("Checking {0} membership for {1}" -f $Group,$Computer)
            $Groups[$Group] += ,'Local'
            Get-LocalGroupMember -LocalGroup $ADSIGroup
        }
        #endregion Get Local Group Members
    }

Here is where everything happens for the queries. Everything that exists in the Process block is here for a reason so I am not needlessly making the same variable creation for each computer passed through the pipeline. Each computer will have a fresh counter and new hash table to handle recursion depth and circular groups, respectfully. Because the first group queried is the one that we defined in the Group parameter, it is already known to be a local group and Get-LocalGroupMember is called first.

I also have some runspace stuff in the code as well, but being how I have already talked about this in other articles, I figured it wasn’t worth mentioning again. If you really want to see the code behind the runspaces, check out those articles here.

Ok, enough talk about the code! It is time to see Get-LocalGroupMembership in action.

Get-LocalGroupMembership | Format-Table –AutoSize

image

You will notice that the groups are only nested 2 levels deep. Also important to notice is that I have some circular groups here with Sysops Admins under Enterprise Admins. Sysops Admins which is also listed under Administrators in which Sysops Admins has… Enterprise Admins listed as a member and thus the circle of member begins! Or at least it would have begun had the code not caught it.

I do not have other remote systems to use –Computername but trust me in that it does work against remote systems.

Get-LocalGroupMembership -Depth 1 | Format-Table –AutoSize

image

This is an example of setting the Depth parameter to only go 1 level deep.

That is all for this function. Feel free to download it from the Script Repository (link below) and let me know what you think!

Download

Technet Script Repository

Posted in powershell, scripts | Tagged , , , , , | 40 Comments

Quick Hits: Set the Default Property Display in PowerShell on Custom Objects

Whenever we write a function or a  script and choose what properties to display, we are usually greeted with the usual display of every single property that we used instead of what the the PowerShell cmdlets and other module cmdlets such as ActiveDirectory (to name one) show which is usually 4 or less properties as a table (or list in some cases).

Custom Object

image

What I want it to be

image


This is due to how the .ps1xml formatting files are written to make sure that there are only a small number of properties that will be displayed to the user.

Fortunately, we can accomplish this same type of output without the need of creating a formatting file and loading it up (a module would be a different story, but for a single function I don’t think it should be necessary).

First thing that we need to do is create a custom object (PowerShell V3 way)

$object = [pscustomobject]@{
    FirstName = 'Bob'
    LastName = 'Smith'
    City = 'San Diego'
    State = 'CA'
    Phone = '555-5555'
    Gender = 'Male'
    Occupation = 'System Administrator'
    DOB = '02/21/1970'
}

image

Nothing new here. Just the usual display of all of the properties when calling $object.

Because this is a custom object, I want to give it a unique typename.

#Give this object a unique typename
$object.PSObject.TypeNames.Insert(0,'User.Information')

Next up is to define the default properties which I can to display when calling this object.

#Configure a default display set
$defaultDisplaySet = 'FirstName','LastName','Gender','City'

I now need to create the property set that will be used later on the object to define what will be displayed. I also need to ensure that I specify the label as a ‘DefaultPropertyDisplaySet’ in the object creation.

#Create the default property display set
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultDisplaySet)

Now we need to create the member info objects needed by Add-Member.

$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

Lastly, we finish up by using Add-Member to add this new member set into the existing custom object.

$object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

Now let’s check out the finished product!

image

Just like I had in the beginning! If I want to see everything then I can just use Select-Object –Property *.

image

Applying this more to a real world scenario, this is how I would end up handling multiple iterations with an object being returned each time.

##Set up the default display set and create the member set object for use later on
#Configure a default display set
$defaultDisplaySet = 'FirstName','LastName','Gender','City'

#Create the default property display set
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

1..10 | ForEach {
    $object = [pscustomobject]@{
        FirstName = (Get-Random -InputObject @('Boe','Bob','Joe','Bill'))
        LastName = (Get-Random -InputObject @('Stevens','Williams','Smith','Prox'))
        City = 'San Diego'
        State = 'CA'
        Phone = '555-5555'
        Gender = 'Male'
        Occupation = 'System Administrator'
        DOB = '02/21/1970'
    }

    #Give this object a unique typename
    $object.PSObject.TypeNames.Insert(0,'User.Information')
    $object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

    #Show object that shows only what I specified by default
    $Object
}

image

What I’ve done is set up most of the default property set up front before I start the actual work and displaying the object in the ForEach statement. This way I am not repeating a process that only has to be done just once.

As you can see, there is not a lot of work involved in doing this and in the end you get a better way of displaying your output without having to mess around with creating or modifying p1xml files!

Posted in powershell, Tips | Tagged , , , , | 11 Comments

PowerShell and Events: WMI Temporary Event Subscriptions

It’s been a while since my last post in my series on PowerShell and Events (February to be exact), but now that I have some projects caught up, it is time to make a return to wrap up this series in what will be 2 or 3 articles, including this one.

Something that I consider to be a powerful technique is using WMI events to monitor some part a system such as disk space, processes or even the event logs. The nice part about this is that you can configure some sort of action to occur when this is tripped such as an email alert or just a display on the console. Considering the massive scope of the WMI repository, you can see that the options are almost limitless with what you can monitor and report on!

Before I show off some examples, we first need to know what is used as far as classes go and how we even get to the point of setting up a temporary subscription.

What do you mean temporary?

When creating a WMI event in PowerShell, you are merely creating something that exists only in the PowerShell console that is running as a background job. This means that as soon as you close the console, the event monitoring goes away with it. Same for rebooting the system and removing the background job or using Unregister-Event to stop the event subscription. Obviously not a permanent monitoring solution by any means, but it does get the job done if you want something that isn’t meant to last forever (or a day Winking smile).

Cmdlets used for WMI Events

Really, the only cmdlet that is required for creating a WMI event is Register-Event. This cmdlet will return a background job object showing that it is now performing the monitoring that you specified and will also perform an action as well if specified. This cmdlet has the same type of parameters as Register-ObjectEvent and Register-EngineEvent, both of which I have explained in detail here and here. Refer to these for more on the parameters such as –Forward, –MessageData and –SupportEvent. If you have used either of the other Register* cmdlets before, then you should feel closer to home as far as the syntax goes. The big thing to grasp is the WMI filter query used to register the event.

Useful classes to know

Working with WMI Events means that you need to know what kind of classes are available to initiate the subscriptions. The following classes are the ones that I have used or read about and will prove useful when registering for the WMI Events:

Win32_ProcessStartTrace http://msdn.microsoft.com/en-us/library/windows/desktop/aa394374(v=vs.85).aspx
Win32_ProcessStopTrace http://msdn.microsoft.com/en-us/library/windows/desktop/aa394376(v=vs.85).aspx
__InstanceModificationEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394651(v=vs.85).aspx
__InstanceCreationEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394649(v=vs.85).aspx
__InstanceDeletionEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394650(v=vs.85).aspx
RegistryTreeChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393041(v=vs.85).aspx
RegistryKeyChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393040(v=vs.85).aspx
RegistryValueChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393042(v=vs.85).aspx

Working down the line of classes, the ProcessTrace classes are used to report when a process has started or stopped.

The __Instance* classes are intrinsic event classes which reports a change to the WMI repository, such as a creation or modification. An example of this would be to use one of the __Instance* classes with Win32_Service to track when a service starts or stops. More on this with examples later.

Lastly on this list, the Registry* classes are extrinsic events which cannot be linked directly to the WMI model. Regardless, you can use this to receive a notification if a part of the registry that has already been defined has been modified of deleted.

With all of these classes that can be used in various combinations, you could say that possibilities on what can be monitored are endless!

I won’t be covering all of these classes but from the examples that I show you later, you can use the other classes to perform various types of monitoring.

Windows Query Language (WQL)

In order to configure the event monitoring with WMI, you must know how to use WQL to setup the filter so the event subscription actually knows what it is looking for. Here is one example:

"select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"

Breaking it down, I will show you what is happening with this filter.

“select * from __instanceModificationEvent” Specify properties which are returned from query.
“within 5” Poll every 5 seconds for event (this doesn’t mean something will be missed, it just means that it may take 5 seconds to acknowledge the event).
“where targetInstance isa ‘win32_Service'” Where filters the scope down and isa applies a query to the subclasses of a specified class

 

For more information regarding WMI queries, check out the following article on more keywords related to WQL: http://msdn.microsoft.com/en-us/library/windows/desktop/aa394606(v=vs.85).aspx

Example using Win32_Service

I already began with my query above using Win32_Service, so lets flesh this out and make an actual subscription using Register-WMIEvent. Some of this may seem familiar if you have checked out my other eventing articles.

##Service change
$WMI = @{
    Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
    Action = {
            If ($Event.SourceEventArgs.NewEvent.PreviousInstance.State -ne $event.SourceEventArgs.NewEvent.TargetInstance.State) {
            $Global:Data = $Event
            Write-Host ("Service: {0}({1}) changed from {2} to {3}" -f $event.SourceEventArgs.NewEvent.TargetInstance.DisplayName,
                                                                            $event.SourceEventArgs.NewEvent.TargetInstance.Name,
                                                                            $event.SourceEventArgs.NewEvent.PreviousInstance.State,
                                                                            $event.SourceEventArgs.NewEvent.TargetInstance.State) -Back Black -Fore Green
            }
    }
    SourceIdentifier = "Service.Action"
}
$Null = Register-WMIEvent @WMI

Nothing really different here, I am using splatting to setup my WMI parameters for Register-WMIEvent cmdlet. I use the same query as mentioned above for the filtering and then my action statement is a scriptblock which tells the subscription what to do when an event is tripped. The action block is set in its own scope and by that I mean when it uses the automatic variable $Event, it is limited only to that script block. You can work around this by setting a Global variable to save $Event to so it can be viewed later.

image

There is a lot happening here with this particular object for the Win32_Service class and the change of the Firewall service from Running to Stopped. From the picture above, you can see where the Action block specified in the code wrote a message on screen saying that the service went from a Running to Stopped state. But how was I able to figure this out? Diving into the $Data variable that was created to hold the Event object, we can begin to see where all of this data resides.

$Data.SourceEventArgs.NewEvent

image

Looking at the SourceEventArgs.NewEvent property, you can see the TIME_CREATED which lets us know when the service change took place. There are two other properties which hold a lot of value to me: PreviousInstance and TargetInstance. The PreviousInstance shows the state of the service prior to a change while the TargetInstance shows the current state of the service. This is why I chose to use an If statement in my Action scriptblock to only worry about the State changes. If I had not done this, I would receive notifications for other changes in services, such as the startmode changing or something else.

PreviousInstance (Previous State)

image

TargetInstance (Current state)

image

Knowing this, I can easily show the previous state of the service as well as show its current state in my display. By the way, the TargetInstance object returned is not a live WMI object, in this case it is System.Management.ManagementBaseObject#root\CIMV2\Win32_Service. But what happens if I try to use the Start() method of this object? Watch and see.

image

If I wanted to restart this service, then I would either use Invoke-WMIMethod or Start-Service using the service name given to achieve that goal. Cool, right? Lets see an example using the Win32_ProcessStartTrace and Win32_ProcessStopTrace to track the processes on my system.

Process Watcher

This example uses the same Register-WMIEvent cmdlet but instead of working with the __Instance* classes for the intrinsic events, I will instead use the readily available Process*Trace classes instead to track processes which have started and stopped running on my local system.

##New Process
$WMI = @{
    Query = "select * from Win32_ProcessStartTrace"
    Action = {
        Write-Host ("Process: {0}({1}) started at {2}" -f $event.SourceEventArgs.NewEvent.ProcessName,
                                                        $event.SourceEventArgs.NewEvent.ProcessID,
                                                        [datetime]::FromFileTime($event.SourceEventArgs.NewEvent.TIME_CREATED)) -Back Black -Fore Green
    }
    SourceIdentifier = "Process.Created"
}
$Null = Register-WMIEvent @WMI
 
##Process End
$WMI = @{
    Query = "select * from Win32_ProcessStopTrace"
    Action = {
        Write-Host ("Process: {0}({1}) was terminated at {2}" -f $event.SourceEventArgs.NewEvent.ProcessName,
                                                                $event.SourceEventArgs.NewEvent.ProcessID,
                                                                [datetime]::FromFileTime($event.SourceEventArgs.NewEvent.TIME_CREATED)) -Back Black -Fore Yellow
    }
    SourceIdentifier = "Process.Deleted"
}
$Null = Register-WMIEvent @WMI

Since my system is not really active right now, I have some self-inflicted processes starting and stopping to give you an idea as to what you will see on the console.

image

With some extra work, this could be a very simple procmon to track what is running.

Tracking the Event Log for Local Account Creations

The last example in the __Instance* classes are going to highlight monitoring the event log on a local system for a account creation attempt. This is being done on a Windows 2012 server so the event log ids may be different if running a 2003 server. In this case, the event id is 4720 for a new account creation (same id for 2008).  What is interesting is that even if I input a password that doesn’t meet the complexity requirements, it will actually create the account, attempt to reset the password to the password given, then upon failure remove the account. With that, lets kick off the monitoring.

##Event log watch -- New User Creation on local system
$WMI = @{
    Query = "select * from __InstanceCreationEvent where TargetInstance isa 'Win32_NtLogEvent' and TargetInstance.logfile = 'Security' and (TargetInstance.EventCode = '4720')"
    Action = {
        $AccountCreated = $event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[0]
        $CreatedBy = ("{0}\{1}" -f $event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[5],$event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[4])
        Write-Host -Foreground Green -Back Black ('New Account: {0} was created by: {1}' -f $ACcountCreated,$CreatedBy)
        $Global:data = $Event
    }
    SourceIdentifier = "Account.Created"
}
$Null = Register-WMIEvent @WMI
 

 

image

Works like a champ! Diving deeper into the object reveals more information about the account itself:

image

As I did with my little console alert, you can narrow down the scope of what you want to report on just by picking what properties are important to you. With the event log monitoring, you have a pretty robust way of monitoring any event that you want on a system.

I’ve shown examples for the Instance* and Process*Trace events and now I will wrap this up with a Registry* class example just to give you an idea as to what it is capable of.

Registry Monitoring

This example will demonstrate how to monitor a registry tree using the RegistryChangeEvent class for the extrinsic event.

##Registry Change
$WMI = @{
    Query ="Select * from RegistryTreeChangeEvent where Hive='HKEY_LOCAL_MACHINE' AND RootPath='Software\\'"
    Action = {
        $Global:Data = $Event
        Write-Host 'Something happened!' -ForegroundColor Green -BackgroundColor Black
    }
    SourceIdentifier = "RegistryTree.Changed"
}
$Null = Register-WMIEvent @WMI

I can make a change anywhere down the HKLM:\Software tree and it will register an event.

image

What I ended up doing is deleting a registry value under Software\Test and that tripped the event. What you can’t see is what was deleted or where it was deleted at! Kind of unfortunate but at least it is something.

The truth is that this is the same type of output you will receive with the other Registry*ChangeEvent classes. The difference is that you have to fine-tune what you are looking at with either the Key or Value that you wish to monitor. At least with these you can tie in some more granular action block items using Get-Item or Get-ItemProperty under the registry provider to verify the new values or check to see if the Key or Value has been removed.

As shown in this article, using WMI temporary events can prove to be a useful thing if you just need a quick monitor with no care about what happens if the console closes or something else happens that causes the event monitoring to stop. While this wasn’t an exhaustive article on temporary WMI events, I hope that it will provide a decent road map in your future scripts to utilize this technique.

My last article regarding PowerShell and Events will conclude with working with permanent WMI events and how they will survive a reboot and keep on monitoring.  It won’t be my next article because I have others already planned to work on, but it will get done!

Posted in powershell | Tagged , , , | 4 Comments

Quick Hits: Determine Tombstone Lifetime in Active Directory

Recently, I wanted to know what the tombstone lifetime was in my environment and decided to find this using PowerShell. Given, I could have done something with dsquery or dug in using the ADSI type accelerator to connect to my domain controller and dig through to find it.

For those of you unfamiliar with this attribute, a good explanation of this is

The number of days before a deleted object is removed from the directory services. This assists in removing objects from replicated servers and preventing restores from reintroducing a deleted object.

Basically, I wanted to know how long I had to recover  if (in my case) one of my domain controllers were down for an extended period of time. For more information on the fun that can occur if this happens and it is down beyond the tombstone lifetime, check out this article: http://technet.microsoft.com/en-us/library/cc786630(v=ws.10).aspx

But back to my question, I already know a number of ways to get this information, but wanted to see if this can be done using the ActiveDirectory module. And the answer is a resounding Duh! Smile This is PowerShell and the ActiveDirectory team has done a fine job with their module which make accessing this attribute an easy issue using the Get-ADObject cmdlet.

In fact, it is so simple it can be done with one line!

(get-adobject "cn=Directory Service,cn=Windows`
 NT,cn=Services,cn=Configuration,dc=rivendell,dc=com" `
-properties "tombstonelifetime").tombstonelifetime

Yes, I am using backticks in my code listing (bad practice!) but I wanted this to fit in the window with no scrolling required. But, as you can see here, the result is exactly what I was looking for! Of course, you will want to change it where I have dc=rivendell,dc=com to whatever matches your environment.

image

There you have it! A nice way to determine your tombstone lifetime using PowerShell and the ActiveDirectory module!

Posted in powershell, Tips | Tagged , , , | 3 Comments