Bookmark and Share

 

(Note: These solutions were written for Advanced Event 2 of the 2010 Scripting Games.)

 

Advanced Event 2 (Windows PowerShell and VBScript)

Image of Richard Siddaway

 

Richard Siddaway, Windows PowerShell MVP


Richard Siddaway is technical architect for Serco in the United Kingdom who works on transformation projects in the local government and commercial arena. With more than 20 years’ experience in various aspects of information technology, Richard specializes in the Microsoft environment at an architectural level, especially around Active Directory (AD), Exchange, SQL Server, and infrastructure optimization.


Richard is always looking for the opportunity to automate a process, preferably with Windows PowerShell. Richard founded and currently leads the UK Windows PowerShell User Group. Microsoft has recognized his technical expertise and community activities by presenting him with a Microsoft Most Valued Professional Award. Richard has presented to the Directory Experts Conference, at various events at Microsoft in the UK and Europe, and for other UK User Groups. Richard has a number of articles and technical publications to his credit, and is currently finalizing his first print book. The subject? Windows PowerShell of course.


Blog
: http://msmvps.com/blogs/RichardSiddaway/Default.aspx

Book details: http://www.manning.com/siddaway


-------------


I will be providing a Windows PowerShell version and a VBScript version of this solution. I will also compare and contrast the two versions.

 


The Problem


We need to read the system event log of a machine to find when it started.  This is approximated by the time the event log service started. From the information supplied this is contained in Event ID 6005.


After that we need to extend our script to:

  • Accept alternative credentials when running against a remote machine.
  • Query any of the traditional event logs.
  • Query for any event ID.
  • Permit the user to specify a specific date or date range from which the events are to be returned.
  • Query for events based upon a wild card character pattern match in the event description.
  • Include command-line help
  • Include error handling


Okay, that should keep us busy. We’ll start with the Windows PowerShell version.


Note:
I am using Windows PowerShell 2.0 for this explanation. All development will be performed in the Windows PowerShell ISE.

 

Windows PowerShell


In Windows PowerShell 1.0, we had Get-EventLog. This is still available in Windows PowerShell 2.0 with enhancements. We query the System event log on the given computer and then filter using Where-Object on Event ID 6005. Get-EventLog can accept an Instance ID but not an Event ID. The IDs are related but not identical. Because we only have the Event ID, we have to use Where-Object. This is shown here:


#Requires -version 2.0
$evt = Get-EventLog -LogName System -ComputerName rslaptop01 |
Where {$_.EventId -eq 6005} |
Select -First 1

Write-Host "Time Started: $($evt.TimeGenerated.DateTime)"


I then use the Select-Object cmdlet to get the first event, which will contain the time of last startup. The Write-Host cmdlet is used to write out the startup time.


We also have the option of using WMI, as shown here:

 

$evt = Get-WmiObject -Class Win32_NTLogEvent -Filter "LogFile = 'System' AND EventCode=6005 " -ComputerName rslaptop01 |  
select -First 1

Write-Host "Time Started: $($evt.ConvertToDatetime($evt.TimeGenerated))"


The only issue with this is converting the date to a readable format, but we have the ConvertToDatetime() method as a standard way of achieving this.


Windows PowerShell 2.0 introduced the Get-WinEvent cmdlet that can read traditional and newer-style logs introduced in Windows Vista and later:


#Requires -version 2.0
$evt = Get-WinEvent -FilterHashtable @{LogName='System'; ID=6005} `
 -MaxEvents 1 -ComputerName rslaptop01

Write-Host "Time Started: $($evt.TimeCreated.DateTime)"


In this version we supply the event log name and the event ID in a hash table. We can filter directly and restrict ourselves to the first event found. Write-Host is then used to display the answer. This runs much faster than using Get-EventLog because the filtering all happens in the cmdlet rather than on the pipeline. A best practice is to always restrict the amount of data you are working with as soon as possible.


In both the scripts, we display the DateTime property. This is to produce a display that removes any ambiguity about the date being in dd/mm/yyyyy format or mm/dd/yyyy format.


The filter hash table can work with a number of parameters including:

·         LogName=<String[]>

·         ProviderName=<String[]>

·         Path=<String[]>

·         Keywords=<Long[]>

·         ID=<Int32[]>

·         Level=<Int32[]>

·         StartTime=<DateTime>

·         EndTime=<DataTime>

·         UserID=<SID>

·         Data=<String[]>

·         *=<String[]>



For the rest of the example, I will work with Get-WinEvent. The following script is what I came up with.


Event2a.ps1


#Requires -version 2.0
function Get-Log {
<#
.SYNOPSIS
  Retrieves events from the event logs on local or remote machines.
.DESCRIPTION
  The function will retrieve events from classic and new style logs
  on local or remote machines. Alternate credentials can be used
  where the current user doesn't have permissions on the remote
  machine.
.PARAMETER log
  Name of the event log. Defaults to system log. 
.PARAMETER id
  Specific event to retrieve.
.PARAMETER computer
  Computer name. Defaults to localhost.
.PARAMETER date
  Date for which events are to be retrieved.
.PARAMETER firstdate
  Starting date to retrieve events.
.PARAMETER lastdate
  Date to end retrieving events
.EXAMPLE
 Get-Log -date "22 March 2010"
 Gets events from the system log on the local machine
 that occurred on a given day.
.EXAMPLE
 Get-Log -firstdate "20 March 2010"
 Gets events from the system log on the local machine
 that occurred after a given day.
.EXAMPLE 
 Get-Log -lastdate "22 March 2010"
 Gets events from the system log on the local machine
 that occurred before a given day.
.EXAMPLE 
 Get-Log -firstdate "20 March 2010" -lastdate "22 March 2010"
 Gets events between the 20th and 22nd of March
.EXAMPLE 
 Get-Log -log Application -firstdate "20 March 2010" -lastdate "22 March 2010"
#>
param (
    [CmdletBinding()]

    [string]$log = "System",
    [int]$id,
    [string]$computer = 'localhost',
    [datetime]$date,
    [datetime]$firstdate,
    [datetime]$lastdate,
    [switch]$cred,
    [regex]$pattern
)
    if ($date -and ($firstdate -or $lastdate)){Throw "Cannot use date together with firsdate or lastdate"}
   
    $fht = @{LogName = $log}

    if ($id){$fht += @{ID = $id}}

    if ($date){
       $fht += @{StartTime = $date;
       EndTime = $date.AddHours(23).AddMinutes(59).AddSeconds(59)}
    }

    if ($firstdate -and !$lastdate){
       $fht += @{StartTime = $firstdate}
    }

    if (!$firstdate -and $lastdate){
       $fht += @{EndTime = $lastdate}
    }

    if ($firstdate -and $lastdate){
       if ($lastdate -lt $firstdate){Throw "Last date before first date"}
       else{ $fht += @{StartTime = $firstdate; EndTime = $lastdate} }
    }

    if ($cred){
        $crd = Get-Credential
        $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer -Credential $crd}
    }
    else {
        $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer}
    }
    if ($pattern){
        Invoke-Command $s | Where {$_.Message -match $pattern}
    }
    else {
        Invoke-Command $s
    }
}   


I created the answer as a function because I decided I would eventually create a module of all of the answers. I can then load all of them in one command. Alternatively, you could put the function in a script and dot source it.


. ./event2a.ps1


This loads the function and we can use it as Get-Log, which will return the contents of the system log on the local machine. The script starts with:


#Requires -version 2.0


This checks to see if we are running on Windows PowerShell 2.0 and stops the script if not. The next set of lines between <# and #> supplies the comment-based help. This is a new feature for functions in Windows PowerShell 2.0.


Typing


Get-Help Get-Log


provides exactly the same sort of help that we get with the cmdlets. We have the same options for getting more detailed help. For example


Get-Help Get-Log –full


displays all of the help information.


The parameters are defined using a param statement. I have supplied a data type for all of the parameters and provided default values where applicable. This catches a number of errors; for example, an attempt to supply an Event ID of “swamp” will be rejected and cause an error.


A test is performed to see if the date parameter has been combined with firstdate and/or lastdate. If so, an error is thrown. This could also be achieved using parameter sets if required.


The hash table $fht is built up starting with the log name. One addition to the script would be to test to see if that log existed on the target machine. At the moment, we assume it does. If an Event ID is supplied, we add that to the hash table. The default is that records for all event codes are returned.


The next task is to set the start and end dates for the period we want to examine the entries. There are a number of possibilities for this so we have a number of tests.


A final job is to test if we need alternative credentials. If so, we use the Get-Credential cmdlet to get them.


A script block is created with the required syntax for Get-WinEvent including the computer name. We do one last check to see if we are filtering on a regular expression (man, I still hate them), and we use Invoke-Command to run our script block. If we have a regular expression filter, we use that in the Where-Object cmdlet as shown.


Some examples of using the script:


To display all entries in the system log on the local machine for a given date:


Get-Log  -date "22 March 2010"


To display all entries in the system log on the local machine that occurred after the given date:


Get-Log  -firstdate "22 March 2010"


To display all entries in the system log on the local machine that occurred before the given date:


Get-Log   -lastdate ""22 March 2010"


To display all entries in the system log on the local machine that occurred March 20–22:


Get-Log   -firstdate "20 March 2010" -lastdate "22 March 2010"


If the dates are the wrong way around, an error is thrown:


Get-Log   -firstdate "22 March 2010" -lastdate "20 March 2010"


Last date before first date:


At C:\scripts\Scripting Games 2010\event2a.ps1:26 char:39
+    if ($lastdate -lt $firstdate){Throw <<<<  "Last date before first date"}
    + CategoryInfo          : OperationStopped: (Last date before first date:String) [], RuntimeException
    + FullyQualifiedErrorId : Last date before first date

 


To display all entries in the application log on the local machine that occurred March 20–22:


Get-Log -log Application -firstdate "20 March 2010" -lastdate "22 March 2010"


The really nice thing about doing this as a function is that I can use it on the pipeline, as the following example shows.

Image of using function on the pipeline

 

I was also asked to have a look at the VBScript option. I used to use VBScript a lot until I discovered Windows PowerShell. I thought it would be an interesting challenge to go back and try the same task in VBScript.


We are more limited in our options with VBScript in that we have to use WMI to access the logs. The WMI class we need is Win32_NTLogEvent as shown here:



strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
      & "{impersonationLevel=impersonate}!\\" _
      & strComputer & "\root\cimv2")
     
Set colEvents = objWMIService.ExecQuery _
      ("SELECT * FROM Win32_NTLogEvent WHERE Logfile = 'System' AND "  _
      &  "EventCode = '6005'")

For Each objEvent in colEvents
      Wscript.Echo "Computer " & objEvent.ComputerName _
      & " Started at " &  WmiDate(objEvent.TimeWritten)
      WScript.Quit
Next
Function WmiDate(startdate)
      WmiDate = Mid(startdate, 7, 2) & "/"  _
         & Mid(startdate, 5, 2) & "/" &  Left (startdate, 4) & " "   _
         & Mid(startdate, 9, 2) & ":" _
         & Mid(startdate, 11, 2)  & ":" _
         & Mid(startdate, 13, 2)
     
End Function


This is the direct equivalent of the Windows PowerShell script, which is shown again here:


 
$evt = Get-WmiObject -Class Win32_NTLogEvent -Filter "LogFile = 'System' AND EventCode=6005 " -ComputerName rslaptop01 |  
select -First 1

Write-Host "Time Started: $($evt.ConvertToDatetime($evt.TimeGenerated))"


The computer name is set to “.” to represent the local machine. We then create an object for WMI. We have to create a full WQL query, which we run to create a collection of objects containing events.


This is a more cumbersome way of working compared to the Get-WmiObject of Windows PowerShell. We use WQL in the –filter parameter of Get-WmiObject, but only the part after the WHERE statement.


Unfortunately it is not easy to select individual objects from the collection we create in VBScript. I start iterating through the collection and write out the start time. Because I want only the first record, I can quit the script. Compare this approach to the select –first 1 we use in Windows PowerShell. I know which I prefer.


A final problem is that WMI returns the date in a format that is not very friendly. We need to create a VBScript function (remember the difference between a subroutine and a function in VBScript) to reformat that data into a more readable format. We do that by using a series of string manipulation functions to perform the reformatting. This is supplied as a function on the Windows PowerShell object.


The ability to use string substitution in the Windows PowerShell example makes writing the output a bit simpler.


So far I would say that the Windows PowerShell script is simpler and quicker to produce. If you have a template for using WMI with VBScript scripts, you might save a bit of development time and the function to convert the date format should ideally be available already.


Let’s see what happens when we move on to the main event. The script for this is shown here:


' get the arguments
Set objNamed = WScript.Arguments.Named

if objNamed.Count = 0 Then
WScript.Echo "type cscript event2a.vbs /help:?  to get the parameters"
WScript.Quit
End if

strComputer = objNamed.Item("computer")
if strComputer = "" Then
WScript.Echo "computer is required"
WScript.Echo "type cscript event2a.vbs /help:?  to get the parameters"
WScript.Quit     
End If

strLog = objNamed.Item("log")
if strlog = "" Then
WScript.Echo "log is required"
WScript.Echo "type cscript event2a.vbs /help:?  to get the parameters"
WScript.Quit     
End If

strEvent = objNamed.Item("event")
date1 = objNamed("date1")
date2 = objNamed("date2")
help = objNamed("help")
userid = objNamed("userid")
password = objNamed("password")

if help <> "" Then
WScript.Echo "Use as "
WScript.Echo "cscript event2a.vbs /computer:""."" /log:system /event:6005 /date1:""03/22/2010"" /date2:""03/23/2010"" "
WScript.Echo "cscript event2a.vbs /help:?"

WScript.Echo "Parameters: "
WScript.Echo "computer - name of remote computer. Use ""."" for local computer"
WScript.Echo "log - name of event log to interrogate"
WScript.Echo ""
WScript.Echo "computer and log ARE REQUIRED"
WScript.Echo ""
WScript.Echo "event - Event ID to retrieve"
WScript.Echo "date1 - starting date to get events for"
WScript.Echo "date2 - ending date to get events for"
WScript.Echo "userid - userid for a remote machine"
WScript.Echo "password - password for a remote machine"
WScript.Quit
End if

if password <> "" Then
Set objLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objLocator.ConnectServer _
    (strComputer, "\root\cimv2", userid, password)
Else
Set objWMIService = GetObject("winmgmts:" _
      & "{impersonationLevel=impersonate}!\\" _
      & strComputer & "\root\cimv2")
End If

' get time zone offset
Set colTimeZone = objWMIService.ExecQuery _
      ("SELECT * FROM Win32_TimeZone")

For Each objTimeZone in colTimeZone
      strBias = objTimeZone.Bias
Next

if strevent = "*" or strevent = "" Then
      strQuery = "SELECT * FROM Win32_NTLogEvent WHERE Logfile = '" & strLog  & "'"
Else
      strQuery = "SELECT * FROM Win32_NTLogEvent WHERE Logfile = '" & strLog  _
      & "' AND EventCode = '" & strEvent & "'"
End If

' create UTC dates
if date1 <> "" AND date2 <> "" Then
      sdate = UTCdate(date1) & "000000.000000-" & Cstr(strBias)
      edate = UTCdate(date2) & "235959.000000-" & Cstr(strBias)
     
      strQuery = strQuery & " AND TimeWritten > '" & sdate & "'" _
            & " AND TimeWritten < '" & edate & "'"
End If

'Wscript.Echo strQuery
'Wscript.Quit    
Set colEvents = objWMIService.ExecQuery(strQuery)

For Each objEvent in colEvents
      Wscript.Echo "Computer Name:" & objEvent.ComputerName
      Wscript.Echo "Event Code:" & objEvent.EventCode
      Wscript.Echo "Message:" & objEvent.Message
      Wscript.Echo "Record Number:" & objEvent.RecordNumber
      Wscript.Echo "Source Name:" & objEvent.SourceName
      Wscript.Echo "Time Written:" & WmiDate(objEvent.TimeWritten)
      Wscript.Echo "Event Type:" & objEvent.EventType
      Wscript.Echo "User:" & objEvent.User
Next

' produces date as mm/dd/yyyyy
Function WmiDate(startdate)
      WmiDate = Mid(startdate, 5, 2) & "/"  _
         & Mid(startdate, 7, 2) & "/" &  Left (startdate, 4) & " "   _
         & Mid(startdate, 9, 2) & ":" _
         & Mid(startdate, 11, 2)  & ":" _
         & Mid(startdate, 13, 2)
     
End Function

Function UTCdate(indate)
     
      UTCdate = Year(indate)
      smonth = Month(indate)
      if Len(smonth) = 1 Then
            smonth = "0" & smonth
      End if

      sday = Day(indate)
      if Len(sday) = 1 Then
            sday = "0" & sday
      End if

      UTCdate = UTCdate  & smonth & sday
End Function


This compares directly to this code as seen previously:


      
$fht += @{EndTime = $lastdate}
    }

    if ($firstdate -and $lastdate){
       if ($lastdate -lt $firstdate){Throw "Last date before first date"}
       else{ $fht += @{StartTime = $firstdate; EndTime = $lastdate} }
    }

    if ($cred){
        $crd = Get-Credential
        $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer -Credential $crd}
    }
    else {
        $s = {Get-WinEvent -FilterHashtable $fht -ComputerName $computer}
    }
    if ($pattern){
        Invoke-Command $s | Where {$_.Message -match $pattern}
    }
    else {
        Invoke-Command $s
    }
}   


Starting at the top of the script, we get the arguments for the script. All of the arguments are named, so we want the WScript.Arguments.Named object. If no arguments are returned, we return an error message that tells the user how to get the help information.


The first argument is the computer. Again, if it is blank, we remind the user it is required. A similar code block checks the log name.


The other arguments are retrieved. I’ve named them to be obvious about what they contain. If help has been asked for, we display the information. By now I’m getting tired of typing WScript.Echo.


If the password is not blank (userid could be checked as well—ideally, you should check both non-blank if one is), we use the SWbemLocator to connect to the remote machine. This is the only way to connect using alternative credentials. If your current credentials give you permissions or you are connecting to the local machine, you don’t need the userid/password so we use the normal winmgmts way to connect to WMI.


The time zone is acquired. I’ve assumed all machines are in the same time zone, but different time zones could be factored in if needed. The offset from UTC is derived as the bias.


We build the query string depending on the parameters and after converting the dates to UTC format, add them to the query string. We can then run our query and iterate through the results to display to the screen.


We have already seen the function to turn a UTC date into a readable date. The second function takes the input date and turns it into UTC format. The date is reformatted to be YYYYMMDD with the slashes stripped out. The times are added for start of day and end of day as appropriate, and we add the time zone bias. The times are added when we use the dates rather than in the function.


An example of running the script is shown in the following image.

Image of example of running the VBScript script



Two approaches to the same problem. So what did I learn from doing this?


1.    
I wouldn’t go back to VBScript after four years of Windows PowerShell.

2.     Windows PowerShell does a lot more for you.

3.     It reminded me why scripting was a minority activity among administrators. If Windows PowerShell achieves nothing else, the opening up of automation and scripting to a wider audience is a great move forward.

 


If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys