Events – Parsing the Event Viewer

The plan for today was to speak about Regular expression and how we can use regex in scripts, but I’ve been diverted to look at some logins and how can I track who’s connecting to some servers and from where.

The output:

VERBOSE: Connecting to computer: tst-server1
VERBOSE: ..WMI status: OK
VERBOSE: ...Found 168 events.
VERBOSE: ....Logon:         86
VERBOSE: ....Logoff:        82
SELECT * FROM Win32_NTLogEvent WHERE (LogFile = 'Security') AND (EventIdentifier = 4634 OR EventIdentifier = 4624) 
AND (Message LIKE '%super-admin%')

ComputerName EventType LogonID    Timestamp             Account     SourceIP      
------------ --------- -------    ---------             -------     --------      
tst-server1  Logon     0xc4542909 4/22/2016 6:31:33 AM  super-admin 
tst-server1  Logon     0xc453df73 4/22/2016 6:31:31 AM  super-admin -             
tst-server1  Logon     0xc453a794 4/22/2016 6:31:29 AM  super-admin -             
tst-server1  Logon     0xa6785b38 4/21/2016 1:11:05 PM  super-admin 
tst-server1  Logon     0xa66d99e4 4/21/2016 1:09:24 PM  super-admin 
tst-server1  Logon     0xa2e04ec2 4/21/2016 11:16:05 AM super-admin 
tst-server1  Logon     0x9ee9524c 4/21/2016 9:27:47 AM  super-admin 
tst-server1  Logon     0x9ee8e0f3 4/21/2016 9:27:44 AM  super-admin -             
tst-server1  Logon     0x9ee86f5d 4/21/2016 9:27:41 AM  super-admin -             
tst-server1  Logon     0x784c9158 4/20/2016 11:16:46 AM super-admin
tst-server1  Logon     0x753a3945 4/20/2016 9:06:45 AM  super-admin

The script that was created to get the above data is pretty simple.  The parameters set for this are: computername which is not mandatory as we can run it locally on a server and the username we are after as a mandatory parameter.

Function Get-LogOnOffEvents

    Param (
        [String]$ComputerName = $Env:ComputerName,

        [String]$userAccount = $Env:username

… then we get the data by using a select in the NTLogEvent table through a WQL query. Instead of parsing the Event viewer using gwmi -class Win32_NTlogEvent we are searching directly in the namespace table as the query is around 8 times faster that regular query using Get-WMIObject -class.. 

$query =    "SELECT * FROM Win32_NTLogEvent "
$query +=   "WHERE (LogFile = 'Security') "
$query +=   "AND (EventIdentifier = 4634 OR EventIdentifier = 4624) "
$query +=   "AND (Message LIKE '%$userAccount%')"

We can query any type of log files and get any type of information from the log file. We just need to know what we are after.

Depending if there is a firewall between the computer where we run the query and the remote one, the script can run successful or fail. This may be due to a WMI job that is hanged or not feeding back any information. As all knows, WMI query doesn’t have a time out in place so we need to define one.

The way I’ve done it here is by using the query as a job and setting a time windows until the job should return some output. If the Job state will be timeout or failed then the next query will not be initiated.

The full function will be:

Function Get-LogOnOffEvents

    Param (
        [String]$ComputerName = $Env:ComputerName,

        [String]$userAccount = $Env:username
    $results = @()

    $query =    "SELECT * FROM Win32_NTLogEvent "
    $query +=   "WHERE (LogFile = 'Security') "
    $query +=   "AND (EventIdentifier = 4634 OR EventIdentifier = 4624) "
    $query +=   "AND (Message LIKE '%$userAccount%')"

    Write-Host '================================='

    Write-Verbose ("Connecting to computer: {0}" -f $ComputerName)
    # WMI related SYSTEM variables
    $wmiStatus = $null
    $wmiJob = $null
    $wmiResult = $null
    # Init the WMI query AS A JOB
    $wmiJob = Get-WmiObject -Query $query -ComputerName $ComputerName -AsJob
    $wmiJob | Wait-Job -Timeout 600 | Out-Null
    if ($wmiJob.JobStateInfo.State -eq 'Running')
        $wmiStatus = 'Timeout'
        Stop-Job -Job $wmiJob
    elseif ($wmiJob.JobStateInfo.State -eq 'Failed')
        $wmiStatus = 'Failed'
        $wmiStatus = 'OK'
        $wmiResult = Receive-Job $wmiJob
    Remove-Job -Job $wmiJob
    Write-Verbose ("..WMI status: {0}" -f $wmiStatus)
    if ($wmiStatus -eq 'OK')
        Write-Verbose ("...Found {0} events." -f $wmiResult.Count)

        $logonEvents = $wmiResult | ? { $_.CategoryString -eq 'Logon' }
        $logoffEvents = $wmiResult | ? { $_.CategoryString -eq 'Logoff' }
        Write-Verbose ("....Logon: {0, 10}" -f $logonEvents.Count)
        Write-Verbose ("....Logoff: {0, 9}" -f $logoffEvents.Count)

        foreach ($event in $logonEvents)
            $results += New-Object PSObject -Property @{
                'ComputerName' = $ComputerName;
                'EventType' = 'Logon';
                'LogonID' = $event.InsertionStrings[7];
                'Timestamp' = $event.ConvertToDatetime($event.TimeGenerated);
                'Account' = $event.InsertionStrings[5];
                'SourceIP' = $event.InsertionStrings[18];
            } | Select ComputerName,EventType,LogonID,Timestamp,Account,SourceIP

        #$logoffEventsResults = @()
        foreach($event in $logoffEvents)
            $results += New-Object PSObject -Property @{
                'ComputerName' = $ComputerName;
                'EventType' = 'Logoff';
                'LogonID' = $event.InsertionStrings[3];
                'Timestamp' = $event.ConvertToDateTime($event.TimeGenerated);
                'Account' = $event.InsertionStrings[1];
                'SourceIP' = 'N/A'
            } | Select ComputerName,EventType,LogonID,Timestamp,Account,SourceIP
    Write-Output $results

Now we just need to run it:

Write-Host (Get-Date -Format 'HH:mm:ss')
$tmp = GetLogOnOffEvents -ComputerName 'tst-server1' -userAccount "super-admin" -Verbose
Write-Host (Get-Date -Format 'HH:mm:ss')

Write-Host '========================================='

$tmp | ft * -AutoSize



Monitoring virtual machines through scripts

How about monitoring VM’s using scripts instead of going to vCenter client? Is this posible?

As an administrator you can select a VM or an ESX and go to Performance tab and see the stats.


The stats provided by vCenter will look something similar with the ones below:


But how about creating your own charts based on some stats you are setting manually?

The first step will be to extract all the info you need by querying the virtual machines.

# import the necessary snapins

Add-PSSnapin VMware.VimAutomation.Core
Add-PSSnapin VMware.VimAutomation.Vds

# define the input file
# this will be the list of servers based on which the script will gather data
$inputFile = "C:\somepathhere\stats-vcenter.csv"

# define & create the output location
$start = Get-Date
$outputLocation = "C:\somepathhere\Get-VMPerfRealtime\{0}-{1:D2}-{2:D2} {3:D2}{4:D2}\" -f $start.Year, $start.Month, $start.Day, $start.Hour, $start.Minute
New-Item -ItemType Directory -Path $outputLocation | Out-Null

# connect to vCenter
Connect-VIServer My-vCenter-server -WarningAction SilentlyContinue | Out-Null

# get the VM objects
# get the list of servers either from an imput document - *.txt or *.csv or by using a query directly pointed to vCenter.
$vms = Get-VM -Name (import-csv $inputFile)

$vms | Foreach-Object {

	$serverStats = $_ | Get-Stat -Realtime -Stat (($_ | Get-StatType -Realtime) | Where { ($_ -like "cpu.entitlement*") -or ($_ -like "cpu.demand*") }) | Sort Timestamp,MetricId,Instance | Select Timestamp,MetricId,Value,Unit | Group Timestamp
	$results = @()
	$serverStats | Foreach-Object {

		$cpuDemand = $_.Group | Where { $_.MetricId -like "cpu.demand*" } | Select -ExpandProperty Value
		$cpuEntitlement = $_.Group | Where { $_.MetricId -like "cpu.entitlement*" } | Select -ExpandProperty Value

		$results += New-Object PSObject -Property @{
			'Timestamp' = ($_.Name -split " ")[1].Substring(0,5);
			'CPU Demand (MHz)' = $cpuDemand;
			'CPU Entitlement (MHz)' = $cpuEntitlement
	$filename = "{0}{1}.csv" -f $outputLocation, $_.Name
	$results | Export-Csv $filename -NoTypeInformation

Disconnect-VIServer My-vCenter-server -Confirm:$false

The above script is getting the CPU demand and CPU Entitlement (in Mhz) stats and store them in a *.csv format somewhere on disk. The *.csv file will contain some stats for each server we’ve queried.

"CPU Demand (MHz)","Timestamp","CPU Entitlement (MHz)"

… then based on the stats we already captured we can create our own charts. This is not something very easy to do in Powershell, but as Powershell is very well integrated with all C# classes we can easily use the System.Windows.Forms namespace. The System.Windows.Forms namespace contains classes for creating Windows-based applications that take full advantage of the rich user interface features available in the Microsoft Windows operating system. (

We will need to also use “Reflection” to add the necessary assemblies in Powershell. You may refer Scripting Guy! Blog.

# load the appropriate assemblies 
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization") | Out-Null

Now that we added the assemblies we will need to create a window for type chart, declare the chart area, create the chart legend and draw all necessarily lines.

We will end with a script like the below one:

# Plot the data

# load the appropriate assemblies 
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization") | Out-Null

foreach ($file in (Get-ChildItem $outputLocation -Filter *.csv)) {

	$chart1 = New-object System.Windows.Forms.DataVisualization.Charting.Chart
	$chart1.Width = 1200
	$chart1.Height = 500
	$chart1.BackColor = [System.Drawing.Color]::White

	# title 
	$chart1.Titles.Add($file.BaseName) | Out-Null
	$chart1.Titles[0].Font = "Arial,13pt"
	$chart1.Titles[0].Alignment = "topLeft"

	# chart area 
	$chartarea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
	$chartarea.Name = "ChartArea1"
	$chartarea.AxisY.Title = "MHz"
	$chartarea.AxisX.Title = "Time"
	$chartarea.AxisY.Interval = 1000
	$chartarea.AxisX.Interval = 10
	$chartarea.AxisY.Maximum = 10000
	$chartarea.AxisY.IsStartedFromZero = $true
	$chartarea.AxisX.MinorGrid.Enabled = $false
	$chartarea.AxisX.MajorGrid.Enabled = $false


	# legend 
	$legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
	$ = "Legend1"

	$datasource = Import-Csv $file.FullName

	$chart1.Series["CpuDemand"].ChartType = "Line"
	$chart1.Series["CpuDemand"].BorderWidth  = 2
	$chart1.Series["CpuDemand"].IsVisibleInLegend = $true
	$chart1.Series["CpuDemand"].chartarea = "ChartArea1"
	$chart1.Series["CpuDemand"].Legend = "Legend1"
	$chart1.Series["CpuDemand"].color = "#62B5CC"
	$datasource | ForEach-Object {$chart1.Series["CpuDemand"].Points.addxy( $_.Timestamp , $_.'CPU Demand (MHz)') | Out-Null }

	$chart1.Series["CpuEntitlement"].ChartType = "Line"
	$chart1.Series["CpuEntitlement"].BorderWidth  = 2
	$chart1.Series["CpuEntitlement"].IsVisibleInLegend = $true
	$chart1.Series["CpuEntitlement"].chartarea = "ChartArea1"
	$chart1.Series["CpuEntitlement"].Legend = "Legend1"
	$chart1.Series["CpuEntitlement"].color = "#FF0000"
	$datasource | ForEach-Object { $chart1.Series["CpuEntitlement"].Points.addxy( $_.Timestamp , $_.'CPU Entitlement (MHz)') | Out-Null }



The $outputLocation variable  will have the same path declared as in the 1st script where the *.csv’s are stored. Of course the *.csv files can be populated with any type of information. The only thing we need to take in consideration is that for each column in the *.csv  file we need to create a Chart series in the 2nd script like the one below.

	$chart1.Series["CpuDemand"].ChartType = "Line"
	$chart1.Series["CpuDemand"].BorderWidth  = 2
	$chart1.Series["CpuDemand"].IsVisibleInLegend = $true
	$chart1.Series["CpuDemand"].chartarea = "ChartArea1"
	$chart1.Series["CpuDemand"].Legend = "Legend1"
	$chart1.Series["CpuDemand"].color = "#62B5CC"
	$datasource | ForEach-Object {$chart1.Series["CpuDemand"].Points.addxy( $_.Timestamp , $_.'CPU Demand (MHz)') | Out-Null }

The System.Windows.Forms namespace is just an example of namespaces that can be added using Reflection into an Powershell script. Of course, not all namespaces from C# will work with Powershell, but most of them will.

In the end, the charts will look something like the below ones:





AntiMalware Health State Error 0x800106f7

If your Event viewer is reporting “There was an error 0x800106f7 in creating the Antimalware Health State WMI instance” this may be due to a corrupted WMI namespace. In this case the namespace belongs to System Center Endpoint Protection  and the namespace is called root\Microsoft\SecurityClient. The error in Event Viewer will look like the below one.


If you want to see the WMI query and who;s generating that query you can go to Operational log withing EventViewer\Applications and Services Logs\Microsoft\Windows\WMI-Activity.

The full error:

- <Event xmlns="">
- <System>
  <Provider Name="Microsoft-Windows-WMI-Activity" Guid="{1418EF04-B0B4-4623-BF7E-D74AB47BBDAA}" /> 
  <TimeCreated SystemTime="2016-04-20T00:11:00.151879300Z" /> 
  <Correlation /> 
  <Execution ProcessID="560" ThreadID="10136" /> 
  <Security UserID="S-1-5-18" /> 
- <UserData>
- <Operation_ClientFailure xmlns="">
  <Operation>Start IWbemServices::ExecNotificationQuery - root\Microsoft\SecurityClient : SELECT * FROM __InstanceOperationEvent WITHIN 30 WHERE TargetInstance ISA "AntimalwareInfectionStatus"</Operation> 

Next step was to look and see who’s ClientProcessId belongs to.


SMS Agent Host belongs to SCCM and this is why SCCM is unable to display the correct “Endpoint Protection Definition Last update time” and “Endpoint Protection Definition Last Version”.

Next step was to run the same query that was initiated by SMS Agent and see the result. This is when I’ve noticed that despite the fact that the namaspace and class “AntiMalwareHealthStatus” is present on the affected computer, the query is not returning anything. In fact it was returning an error –> Invalid Operation.

PS C:\> gwmi  -Query "select * from AntiMalwareHealthStatus" -namespace "root\Microsoft\SecurityClient"
gwmi :
At line:1 char:1
+ gwmi  -Query "select * from AntiMalwareHealthStatus" ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Get-WmiObject], COMException
    + FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

PS C:\>

Running the above query is generating the same event ID in event viewer so this is how you replicate the error.

The fix can be simple at this stage: restart WMI, but in case we have numerous services depending on WMI this will be impossible to happen during normal business hours.

taskkill /im WmiPrvSE.exe /f

Let’s see which WmiPrvSE service was created by SCEP. I’ve found out this using Sysinternals Process Explorer.


The fix will be to recycle the process. If we have more than one server affected – in my case there were 50+ affected by this issue – you will need to use some sort of automation.

Step 1. –> Killing the affected process and releasing the namespace

(Get-Process | where {$_.Modules.filename -like "*mpprovider*"}).kill()

The above will kill the right WmiPrvSE process sa there is jut one that has the MpProvider.dll in use.

Step 2. –> testing if the query is now successful.

PS C:\> gwmi -query "select * from AntiMalwareHealthStatus" -namespace "root\Microsoft\SecurityClient"

__GENUS                            : 2
__CLASS                            : AntimalwareHealthStatus
__SUPERCLASS                       : ProtectionTechnologyStatus
__DYNASTY                          : SerializableToXml
__RELPATH                          : AntimalwareHealthStatus=@
__PROPERTY_COUNT                   : 31
__DERIVATION                       : {ProtectionTechnologyStatus, SerializableToXml}


When the query is initiated a new WmiPrvSE process is created.



If we check the WMI-Activity log we will see that the return code is 0x0

WmiPerfInst provider started with result code 0x0. HostProcess = wmiprvse.exe; ProcessID = 46344; ...

This method is non disruptive and can be completed any time. The root cause may be generated by some patches/updates as this can corrupt WMI repository.



The WMI Query Language (WQL) is a subset of the American National Standards Institute Structured Query Language (ANSI SQL)—with minor semantic changes. The following table lists the WQL keywords.


WQL keyword Meaning
AND Combines two Boolean expressions, and returns TRUE when both expressions are TRUE.
ASSOCIATORS OF Retrieves all instances that are associated with a source instance.

Use this statement with schema queries and data queries.

__CLASS References the class of the object in a query.
FROM Specifies the class that contains the properties listed in a SELECT statement. Windows Management Instrumentation (WMI) supports data queries from only one class at a time.
GROUP Clause Causes WMI to generate one notification to represent a group of events.

Use this clause with event queries.

HAVING Filters the events that are received during the grouping interval that is specified in the WITHIN clause.
IS Comparison operator used with NOT and NULL. The syntax for this statement is the following:


(where NOT is optional)

ISA Operator that applies a query to the subclasses of a specified class. For more information, see ISA Operator for Event QueriesISA Operator for Data Queries, and ISA Operator for Schema Queries.
KEYSONLY Used in REFERENCES OF and ASSOCIATORS OF queries to ensure that the resulting instances are only populated with the keys of the instances, which reduces the overhead of the call.
LIKE Operator that determines whether or not a given character string matches a specified pattern.
NOT Comparison operator that use in a WQL SELECT query, for example:

SELECT * FROM meta_class WHERE NOT __class < “Win32” AND NOT __this ISA “Win32_Account”

NULL Indicates an object does not have an explicitly assigned value. NULL is not equivalent to zero (0) or blank.
OR Combines two conditions.

When more than one logical operator is used in a statement, the OR operators are evaluated after the AND operators.

REFERENCES OF Retrieves all association instances that refer to a specific source instance. Use this statement with schema and data queries. The REFERENCES OF statement is similar to the ASSOCIATORS OF statement. However, it does not retrieve endpoint instances; it retrieves the association instances.
SELECT Specifies the properties that are used in a query.

For more information, see SELECT Statement for Data QueriesSELECT Statement for Event Queries, or SELECT Statement for Schema Queries.

TRUE Boolean operator that evaluates to -1 (minus one).
WHERE Narrows the scope of a data, event, or schema query.
WITHIN Specifies a polling or grouping interval.

Use this clause with event queries.

FALSE Boolean operator that evaluates to 0 (zero).


You can run a WQL query using the built in command Get-WmiObject (alias: gwmi)

This query will return all the win32 classes except the Win32_account

PS C:\Users\Administrator> gwmi -Query 'SELECT * FROM meta_class WHERE NOT __class < "Win32" AND NOT __this ISA "Win32_A

   NameSpace: ROOT\cimv2

Name                                Methods              Properties
----                                -------              ----------
Win32_PrivilegesStatus              {}                   {Description, Operation, ParameterInfo, PrivilegesNotHeld...}
Win32_JobObjectStatus               {}                   {AdditionalDescription, Description, Operation, ParameterIn...
Win32_Trustee                       {}                   {Domain, Name, SID, SidLength...}
Win32_ACE                           {}                   {AccessMask, AceFlags, AceType, GuidInheritedObjectType...}
Win32_SecurityDescriptor            {}                   {ControlFlags, DACL, Group, Owner...}
Win32_ComputerSystemEvent           {}                   {MachineName, SECURITY_DESCRIPTOR, TIME_CREATED}
Win32_ComputerShutdownEvent         {}                   {MachineName, SECURITY_DESCRIPTOR, TIME_CREATED, Type}
Win32_IP4RouteTableEvent            {}                   {SECURITY_DESCRIPTOR, TIME_CREATED}
Win32_SystemTrace                   {}                   {SECURITY_DESCRIPTOR, TIME_CREATED}
Win32_ProcessTrace                  {}                   {ParentProcessID, ProcessID, ProcessName, SECURITY_DESCRIPT...
Win32_ProcessStartTrace             {}                   {ParentProcessID, ProcessID, ProcessName, SECURITY_DESCRIPT...
Win32_ProcessStopTrace              {}                   {ExitStatus, ParentProcessID, ProcessID, ProcessName...}



Asset Management – Get Installed Software

On the market we can find numerous products that are doing asset management without any Admin headache. As part of asset management an Administrator will be interested in getting all the installed software on an workstation or server.

If it’s just one device then it’s easier to connect to it and open the Program and Feature GUI and look inside. But if you need to compile a list with all the software installed on  a list of computers used by Finance team then this is another thing. Of course you – as an Admin – will need to get the list of computers used by this team and look inside to see what they have installed. Not to mention that I’ve saw companies where the end user were able to install what ever he want on his computer as he was a local admin.

Whiteout a tool that is inventorying all the assets I’ve used a script to query the registry and get the list.


Function Get-InstalledSoftware{ 
    If (!$Computers) {$Computers = $ENV:ComputerName} 
    $Base = New-Object PSObject; 
    $Base | Add-Member Noteproperty ComputerName -Value $Null; 
    $Base | Add-Member Noteproperty Name -Value $Null; 
    $Base | Add-Member Noteproperty Publisher -Value $Null; 
    $Base | Add-Member Noteproperty InstallDate -Value $Null; 
    $Base | Add-Member Noteproperty EstimatedSize -Value $Null; 
    $Base | Add-Member Noteproperty Version -Value $Null; 
    $Results =  New-Object System.Collections.Generic.List[System.Object]; 
    ForEach ($ComputerName in $Computers){ 
        $Registry = $Null; 
            $Registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,$ComputerName);
            Write-Host -ForegroundColor Red "$($_.Exception.Message)";
        If ($Registry){ 
            $UninstallKeys = $Null; 
            $SubKey = $Null; 
            $UninstallKeys = $Registry.OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Uninstall",$False); 
                $SubKey = $UninstallKeys.OpenSubKey($_,$False); 
                $DisplayName = $SubKey.GetValue("DisplayName"); 
                If ($DisplayName.Length -gt 0){ 
                    $Entry = $Base | Select-Object * 
                    $Entry.ComputerName = $ComputerName; 
                    $Entry.Name = $DisplayName.Trim();  
                    $Entry.Publisher = $SubKey.GetValue("Publisher");  
                    [ref]$ParsedInstallDate = Get-Date 
                    If ([DateTime]::TryParseExact($SubKey.GetValue("InstallDate"),"yyyyMMdd",$Null,[System.Globalization.DateTimeStyles]::None,$ParsedInstallDate)){                     
                        $Entry.InstallDate = $ParsedInstallDate.Value 
                    $Entry.EstimatedSize = [Math]::Round($SubKey.GetValue("EstimatedSize")/1KB,1); 
                    $Entry.Version = $SubKey.GetValue("DisplayVersion"); 

In order for this script to run you will need two things: Remote Registry services to be running on the end device and also access to those registry (admin right).

You can execute the script like below, where “localhost” is a name of a coputer.

Get-InstalledSoftware localhost

The output will be an array like this one:

PS C:\Users\cpadurariu$> Get-InstalledSoftware localhost | ft * -AutoSize

ComputerName Name                                                                 Publisher                                 InstallDate          
------------ ----                                                                 ---------                                 -----------          
localhost    7-Zip 15.14 (x64)                                                    Igor Pavlov                                                    
localhost    Beyond Compare 4.1.1                                                 Scooter Software                          9/30/2015 12:00:00 AM
localhost    HP ProLiant iLO 3/4 Management Controller Package                    Hewlett-Packard Company                                        
localhost    HP ProLiant iLO 3/4 Channel Interface Driver                         Hewlett-Packard Company                                        
localhost    HP ProLiant Agentless Management Service                             Hewlett-Packard Company

In case you get the below output, the error is generated by lack of access on the remote computer.

Exception calling "OpenSubKey" with "2" argument(s): "Requested registry access is not allowed."
At line:22 char:50
+             $UninstallKeys = $Registry.OpenSubKey <<<< ("Software\Microsoft\Windows\CurrentVersion\
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException
You cannot call a method on a null-valued expression.
At line:23 char:42
+             $UninstallKeys.GetSubKeyNames <<<< ()|%{ 
    + CategoryInfo          : InvalidOperation: (GetSubKeyNames:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull




Get logged-on users from a computer

As administrators we sometime forgot to logoff from servers. In order to release the resources lost because of an inactive or disconnected session we can use two methods: either a GPO – if the computer belongs to an active directory domain – or a script to query all the servers and return a list will the users found on the queried servers.

Set time-out settings for disconnected, active, and idle sessions using a group policy is described very good on Microsoft technet url: 

The below function will retrieve all connected users to a $computer. The data return will be divided in two: the computername and the username (including the domain).

function get-loggedonusers
    Get-WmiObject Win32_LoggedOnUser -ComputerName $Computer | 
    Select Antecedent,__SERVER -Unique |
    where { -not ($_.Antecedent -like "*$computer*" )} | 
    %{"{0}`t{1}\{2}" -f $_.__SERVER, $_.Antecedent.ToString().Split("`"")[1], $_.Antecedent.ToString().Split("`"")[3]}

If we need alt the Sessionname, Session ID, state of the session, Idle time and logon time we can use:

query user /server:$server

using Query user will return a table like the below one.

PS C:\> query user /server:$server
 Johny.bravo                               2  Disc        10:59  15/04/2016 03:01
 bill.gates                                3  Disc         6:38  15/04/2016 03:35                                  4  Disc         7:28  15/04/2016 06:46
 superman.theMan       rdp-tcp#34          6  Active       3:34  15/04/2016 06:58
 another.namen         rdp-tcp#14          7  Active       3:34  15/04/2016 07:04
 cristi.cuture                             8  Disc           55  15/04/2016 07:06
 claus.santa           rdp-tcp#73          9  Active       1:09  15/04/2016 08:23
 super.admin           rdp-tcp#61         10  Active         21  15/04/2016 12:01
 dogs.andcats                             11  Disc           23  15/04/2016 12:19
 great.cezar           rdp-tcp#24         12  Active          3  15/04/2016 07:26
 mohammed.thegreat     rdp-tcp#39         13  Active          .  15/04/2016 08:58
 pierre.cardin         rdp-tcp#41         14  Active         11  15/04/2016 09:05
 valerius.maximus                         15  Disc         3:58  15/04/2016 09:21
 johny.walker          rdp-tcp#51         16  Active          4  15/04/2016 09:39
 another.user          rdp-tcp#86         17  Active         20  15/04/2016 09:40
 active.user           rdp-tcp#65         18  Active         12  15/04/2016 12:45
 the.user              rdp-tcp#78         19  Active         48  15/04/2016 13:30
 christmas.maximus                        20  Disc            8  15/04/2016 13:58

We just need to create a foreach loop to go through a full list of servers and save all the data in array. Also we can configure a SMTP replay to remind the administrator to log off if his work was done.