There's always a lot of talk about monitoring when stuff gets installed and uninstalled on a Windows machine, or when "configuration changes" take place on a system, or even when unplanned reboots (crashes) take place... how do we audit that? As awesome as the Windows event logs are, they can be a bit unwieldy to sift through all the noise and cryptic messages.
There are lots of third-party tools for auditing software changes. Those tools can cost a lot of money. But did you know Windows already does this for you? If you run perfmon /rel on your Vista/7/2008/R2 machine, you will be greeted with this pretty picture:

Notice that you can even export all the data as a nice little XML file. So that's pretty neat. You can see all the application crashes, system crashes, when software was installed and uninstalled, etc... but that's all GUI stuff. I know what really you want is something more programmatic, customizable and automatable. It just so happens that there's a WMI class called Win32_ReliabilityRecords. Let's use Powershell to take a peek:
# Looking at Windows software installations and uninstallations and other reliability data
# Ryan Ries, Jan 5 2012
#
# Usage: .\ReliabilityData.ps1 <argument>
# Valid arguments are "ShowAll", "ShowSystemCrashes", "ShowWhateverYourImaginationIsTheLimit", ...
# Arguments are not case sensitive.
param([parameter(Mandatory=$true)]
[string]$Argument)
Function WMIDateStringToDateTime([String] $strWmiDate)
{
$strWmiDate.Trim() > $null
$iYear = [Int32]::Parse($strWmiDate.SubString( 0, 4))
$iMonth = [Int32]::Parse($strWmiDate.SubString( 4, 2))
$iDay = [Int32]::Parse($strWmiDate.SubString( 6, 2))
$iHour = [Int32]::Parse($strWmiDate.SubString( 8, 2))
$iMinute = [Int32]::Parse($strWmiDate.SubString(10, 2))
$iSecond = [Int32]::Parse($strWmiDate.SubString(12, 2))
$iMicroseconds = [Int32]::Parse($strWmiDate.Substring(15, 6))
$iMilliseconds = $iMicroseconds / 1000
$iUtcOffsetMinutes = [Int32]::Parse($strWmiDate.Substring(21, 4))
if ( $iUtcOffsetMinutes -ne 0 )
{
$dtkind = [DateTimeKind]::Local
}
else
{
$dtkind = [DateTimeKind]::Utc
}
return New-Object -TypeName DateTime -ArgumentList $iYear, $iMonth, $iDay, $iHour, $iMinute, $iSecond, $iMilliseconds, $dtkind
}
If($Argument -eq "ShowAll")
{
$reliabilityData = Get-WmiObject Win32_ReliabilityRecords
ForEach ($entry in $reliabilityData)
{
Write-Host "Computer Name: " $entry.ComputerName
Write-Host "Event ID: " $entry.EventIdentifier
Write-Host "Record Number: " $entry.RecordNumber
Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
Write-Host "Source: " $entry.SourceName
Write-Host "Product Name: " $entry.ProductName
Write-Host "User: " $entry.User
Write-Host "Message: " $entry.Message
Write-Host " "
}
}
If($Argument -eq "ShowSystemCrashes")
{
$reliabilityData = Get-WmiObject Win32_ReliabilityRecords
ForEach ($entry in $reliabilityData)
{
If($entry.Message.StartsWith("The previous system shutdown") -And $entry.Message.EndsWith("was unexpected."))
{
Write-Host "Computer Name: " $entry.ComputerName
Write-Host "Event ID: " $entry.EventIdentifier
Write-Host "Record Number: " $entry.RecordNumber
Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
Write-Host "Source: " $entry.SourceName
Write-Host "Product Name: " $entry.ProductName
Write-Host "User: " $entry.User
Write-Host "Message: " $entry.Message
Write-Host " "
}
}
}
If($Argument -eq "ShowApplicationInstalls")
{
$reliabilityData = Get-WmiObject Win32_ReliabilityRecords
ForEach ($entry in $reliabilityData)
{
If($entry.Message.StartsWith("Windows Installer installed the product."))
{
Write-Host "Computer Name: " $entry.ComputerName
Write-Host "Event ID: " $entry.EventIdentifier
Write-Host "Record Number: " $entry.RecordNumber
Write-Host "Date and Time: " $(WMIDateStringToDateTime($entry.TimeGenerated))
Write-Host "Source: " $entry.SourceName
Write-Host "Product Name: " $entry.ProductName
Write-Host "User: " $entry.User
Write-Host "Message: " $entry.Message
Write-Host " "
}
}
}
So those are just some ideas that I threw together, but is by no means a complete solution. Use that as a starting point, play with the script, expand on it and make it even better! And one last thing, ideally I should not be using Write-Host here but instead be preserving the objects, that way I could combine this script with other commandlets on the pipeline, etc. I'll put that in as an enhancement request...