Quick and Dirty Web Site Monitoring with PowerShell

The other day Mark noticed that redirections for our http://www.sysinternals.com/ URL were intermittently failing. In order to get more objective data, I built a script that tested the URL every 5 seconds, and reported back Success or Failure as well as performance ﴾how long it took to completely download base HTML content﴿. I found that PowerShell provided an easy way to use the WebClient .Net object and evaluate the returned HTML content.

Web-Monitoring-1

Example 1: Single Site Monitoring

The following example opens a URL every 5 minutes, tests the content, and measures the time it took to download the HTML for the page. Notice that all the HTML is dumped into a big fat string. The string is then searched for specific text that is known to be in the requested page. Note that this script runs forever and can be stopped with a <Ctrl> ‘C’.

Example PowerShell script:
$webClient = newobject
System.Net.WebClient
$webClient.Headers.Add("useragent","PowerShell Script")
while (1 eq 1) {
   $output = ""
   $startTime = getdate
   $output = $webClient.DownloadString "http://www.sysinternals.com/")
   $endTime = getdate
   if ($output like "*Mark Russinovich*") {
      "Success`t`t" + $startTime.DateTime + "`t`t" + ($endTime $startTime).TotalSeconds + " seconds"
   } else {
      "Fail`t`t" + $startTime.DateTime + "`t`t" + ($endTime $startTime).TotalSeconds + " seconds"
   }
   sleep(300)
}

Web-Monitoring-2

Example 2: Monitoring and Alerting for Multiple Web Sites

This script monitors multiple URLs ﴾or web sites﴿, and incorporates e‐mail alerting and logging. Unlike the above script, it is designed to be triggered from the Windows Task Scheduler ﴾or some other job scheduler﴿ rather than Quick and Dirty Web Site Monitoring with PowerShell running forever in a loop. Notice that one of the URLs is actually a zipped file and PowerShell has no problem evaluating it as a string.

Example PowerShell script:
# Collects all named paramters (all others end up in $Args)
param($alert)
# Display Help
if (($Args[0] eq "?") or ($Args[0] eq "help")) {
   ""
   "Usage: SysinternalsSiteTest.ps1 alert <address> log"
   " alert <address> Send email alerts"
   " log Log results"
   ""
   "Example: SysinternalsSiteTest.ps1 alert somebody@nospam.com log"
   ""
   exit
}
# Create the variables
$global:GArgs = $Args

$urlsToTest = @{}
$urlsToTest["Sysinternals Redirect"] = "http://www.sysinternals.com"
$urlsToTest["TechNet Redirect"] = "http://www.microsoft.com/sysinternals"
$urlsToTest["Sysinternals Home"] = "http://www.microsoft.com/technet/sysinternals/default.mspx"
$urlsToTest["Sysinternals Forum"] = "http://forum.sysinternals.com"
$urlsToTest["Sysinternals Blog"] = "http://blogs.technet.com/sysinternals"
$urlsToTest["Sysinternals Downloads"] = "http://download.sysinternals.com/Files/NtfsInfo.zip"
$successCriteria = @{}
$successCriteria["Sysinternals Redirect"] = "*Mark Russinovich*"
$successCriteria["TechNet Redirect"] = "*Mark Russinovich*"
$successCriteria["Sysinternals Home"] = "*Mark Russinovich*"
$successCriteria["Sysinternals Forum"] = "*Sysinternals Utilities*"
$successCriteria["Sysinternals Blog"] = "*Sysinternals Site Discussion*"
$successCriteria["Sysinternals Downloads"] = "*ntfsinfo.exe*"
$userAgent = "PowerShell User"
$webClient = newobject System.Net.WebClient
$webClient.Headers.Add("useragent",$userAgent)
foreach ($key in $urlsToTest.Keys) {
   $output = ""
   $startTime = getdate
   $output = $webClient.DownloadString($urlsToTest[$key])
   $endTime = getdate
   if ($output like $successCriteria[$key]) {
      $key + "`t`tSuccess`t`t" + $startTime.DateTime + "`t`t" + ($endTime $startTime).TotalSeconds + " seconds"
      if ($GArgs eq "log") {
         $key + "`t`tSuccess`t`t" + $startTime.DateTime + "`t`t" + ($endTime - $startTime).TotalSeconds + " seconds" >> WebSiteTest.log
      }
   } else {
      $key + "`t`tFail`t`t" + $startTime.DateTime + "`t`t" + ($endTime $startTime).TotalSeconds + " seconds"
      if ($GArgs eq "log") {
         $key + "`t`tFail`t`t" + $startTime.DateTime + "`t`t" + ($endTime - $startTime).TotalSeconds + " seconds" >> WebSiteTest.log
      }
      if ($alert) {
         $emailFrom = "computer@nospam.com"
         $emailTo = $alert
         $subject = "URL Test Failure " + $startTime
         $body = "URL Test Failure: " + $key + " (" + $urlsToTest[$key] + ") at " + $startTime
         $smtpServer = "somesmtpserver.nospam.com"
         $smtp = newobject Net.Mail.SmtpClient($smtpServer)
         $smtp.Send($emailFrom,$emailTo,$subject,$body)
      }
   }
}

Web-Monitoring-3

Enjoy!

Quick and Dirty Software Inventory with PsInfo and PowerShell

PsInfo is great for gathering asset information from Windows computers, both locally and remotely. PowerShell is great for automation and cleaning up output (among other things) as well as working with database driven data.

The following examples show how to gather an itemized list of the installed software on remote machines, process the data, then either display it to the screen or store it in a database. It’s worth noting that PsInfo can also work on multiple remote computers from its native command line, or even read a list of computers from a file (check out the PsInfo site for more info). Since the final example seeks to show PsInfo in a database driven envoriment, PowerShell comes in very handy.

Note: In order for this example to work the necessary network connectivity and credentials will need to be in place.

Consider the following examples:

  1. The output is merely displayed on the screen. With this method the output can be redirected to a file and imported into an application like Excel for further analysis or record keeping.
  2. A database is used to drive the computers polled as well as store the output. The database table is very flat (one table) with 2 fields: ‘Computer’ and ‘Software’. For large amounts of data, this will need to be normalized.

Software-Inventory-1

With the following output (imported into Excel):

Software-Inventory-2

Example 1: Standard Screen Output

The following PowerShell script gathers a software inventory from 3 remote computers (‘happyhour’, ‘shaken’, and ‘extradry’). Presumably, your computer names will be different. After gathering and parsing the data, it’s then displayed on the screen for all machines successfully queried.

Before running this script, test your connectivity and credentials with a single PsInfo command:

PsInfo -s Applications \\somecomputer
Example PowerShell script:
$computersToQuery = ("happyhour","shaken","extradry")
$softwareInventory = @{}
foreach ($computer in $computersToQuery) {
  $psinfoOutput = ./psinfo.exe -s Applications \\$computer
  $foundSoftwareInventory = 0
  $computerName = ""
  foreach ($item in $psinfoOutput) {
    if ($foundSoftwareInventory -eq 1) {
      # Force the results to a string
      # Remove any single quotes which interfere with T-SQL statements
      # Load the result into a hash whereby removing any duplicates
      [string]$softwareInventory[$computerName][$item.Replace("'","")] = ""
    }
    if ($item -like "System information for *") {
     $computerName = $item.Split("\")[2].TrimEnd(":")
    } elseif ($item -eq "Applications:") {
     $foundSoftwareInventory = 1
     $softwareInventory[$computerName] = @{}
    }
  }
}
foreach ($computer in $softwareInventory.Keys) {
  foreach ($softwareItem in $softwareInventory[$computer].Keys) {
   $computer + ":" + $softwareItem
  }
}

Your output should look something like:

Software-Inventory-3

Example 2: Save Output to a Database

This example is additive to the first in that it adds the following 3 items:

  1. Pulls the list of computer to query from a database table
  2. Adds the current data and time to the result
  3. Records the audit results into a database

The following is the database schema for this example:

4Software-Inventory-3

Example PowerShell script:
# Open the database connection
$dbConn = new-object System.Data.SqlClient.SqlConnection "server=kcdb;database=Inventory;Integrated Security=sspi"
$dbConn.Open()
$sqlQuery = $dbConn.CreateCommand()

# Get all known computers
$sqlQuery.CommandText = "select * from Inventory..Computers"
$reader = $sqlQuery.ExecuteReader()
$computersToQuery = @()
while ($reader.Read()) {
   $computersToQuery += $reader["Computer"]
}

# Close the database connection
$dbConn.Close()

$softwareInventory = @{}
foreach ($computer in $computersToQuery) {
   $psinfoOutput = ./psinfo.exe -s Applications \\$computer
   $foundSoftwareInventory = 0
   $computerName = ""
   foreach ($item in $psinfoOutput) {
      if ($foundSoftwareInventory -eq 1) {
         # Force the results to a string
         # Remove any single quotes which interfere with T-SQL statements
         # Load the result into a hash whereby removing any duplicates
         [string]$softwareInventory[$computerName][$item.Replace("'","")] = ""
      }

      if ($item -like "System information for *") {
         $computerName = $item.Split("\")[2].TrimEnd(":")
      } elseif ($item -eq "Applications:") {
         $foundSoftwareInventory = 1
         $softwareInventory[$computerName] = @{}
      }
   }
}

$dbConn = new-object System.Data.SqlClient.SqlConnection "server=kcdb;database=Inventory;Integrated Security=sspi"
$dbConn.Open()
$sqlQuery = $dbConn.CreateCommand()
foreach ($computer in $softwareInventory.Keys) {
   foreach ($softwareItem in $softwareInventory[$computer].Keys) {
      "Loading-" + $computer + ":" + $softwareItem
      # Try an Insert than an Update
      trap {
         $sqlQuery.CommandText = "update Inventory..SoftwareInventory set AuditDate = getdate() where Computer = '" + $computer + "' and Software = '" + $softwareItem + "'"
         $result = $sqlQuery.ExecuteNonQuery()
         continue
      }
      $sqlQuery.CommandText = "insert into Inventory..SoftwareInventory (      Computer,Software,AuditDate) values ('" + $computer + "','" + $softwareItem + "',getdate())"
      $result = $sqlQuery.ExecuteNonQuery()
   }
}

$dbConn.Close()

For more information:

Enjoy!

A Few Good Windows Remote Management Commands

In Vista and beyond, a lot of instrumentation, configuration, and utilization information is exposed via WS-Man. WS-Man (aka: WS-Management, Windows Remote Management, and WinRM) incorporates many features, but I like to think of it as the management protocol/framework of the future (look out SNMP!). What makes WS-Man so great is the fact that it’s all standards based, rides on HTTP/HTTPS (very firewall/NAT friendly), and packages its data in SOAP/XML packets (easy to shove into a database or use with a script).

Out of the box; Vista WS-Man exposes WMI information as well as Windows Remote Shell capabilities. What this means is that with WS-Man it’s much easier to get instrumentation from remote machines as well as use that info in scripts.

Here are some sample commands to play with. If you cannot get the ‘Test WS-Man…’ step to work, none of the steps following will work either (you’re probably not using the right credentials to access the remote machine). One more caveat, the remote commands work best on domain joined machines. For workgroup machines, the WinRM service needs additional configuration.

Quickly configure the WS-Man service (Run from an Elevated Command prompt):

winrm QuickConfig

Quickly delete the WS-Man listener (Run from an Elevated Command prompt):

winrm invoke Restore winrm/Config @{}

Display your machine’s basic hardware info:

winrm enumerate wmicimv2/Win32_ComputerSystem

Display your operating system properties:

winrm get wmicimv2/Win32_OperatingSystem

Output your OS info in XML:

winrm get wmicimv2/Win32_OperatingSystem -format:pretty

Ping WS-Man on a remote machine:

winrm id -auth:none -remote:<some machine>

Test WS-Man access to a remote machine**:

winrm id -remote:<some machine>

Grab a remote machine’s WS-Man config:

winrm get winrm/Config -r:<some machine>

Grab a remote machine’s CPU load:

winrm g wmicimv2/Win32_Processor?DeviceID=CPU0 -fragment:LoadPercentage -r:<some computer>

Grab a remote machine’s free memory:

winrm g wmicimv2/Win32_OperatingSystem -fragment:FreePhysicalMemory -r:<some computer>

Stop a service on a remote machine:

winrm invoke stopservice wmicimv2/Win32_Service?name=w32time -r:<some computer>

Start a service on a remote machine:

winrm invoke startservice wmicimv2/Win32_Service?name=w32time -r:<some computer>

Reboot a remote machine:

winrm invoke reboot wmicimv2/Win32_OperatingSystem -r:<some computer>

Run a command on a remote machine (this uses winrS, not winrM):

winrs -r:<some computer> ipconfig /all

Use PowerShell to grab the WS-Man Win32_OperatingSystem XML output (Run from PowerShell):

[xml]$osInfo = winrm get wmicimv2/Win32_OperatingSystem /format:pretty

Display the OS version property:

$osInfo.Win32_OperatingSystem.Version

Display the last boot time:

$osInfo.Win32_OperatingSystem.LastBootupTime.DateTime

Put free memory metric into an XML variable:

[xml]$freemem = cmd /c "winrm get wmicimv2/Win32_OperatingSystem -fragment:FreePhysicalMemory -f:pretty -r:<some computer>"

Display the free memory value:

$freemem.XMLFragment.FreePhysicalMemory

Note: This step verifies that you have good connectivity to the remote machine, WS-Man is running and properly configured on the remote machine, AND you have the correct permissions to fully leverage WS-Man on the remote machine. If this step fails, it’s probably a permissions issue.

Details:

WS-Man (WinRM) Architecture

The following diagram shows a high-level overview of the WS-Man (WinRM) architecture. In the diagram the ‘Client’ is querying the ‘Server’ for WS-Man information. Note that HTTP.sys and WinHTTP support the HTTP(s) transport for WS-Man, not IIS. In addition, IIS (or another web publishing service) can co-exist with WS-Man and share port 80 (granted, in Windows 7 and beyond, the default ports are 5985 and 5986 for http and https respectively, although I still prefer to use 80 and 443).

WSMan-Commands-1

Quickly configure the WS-Man service

Remember, this needs to be run from an ‘Elevated’ Command Prompt.

As you can see, this simple command does quite a bit. Please note every modification (hightlighted) since this might increase the attack surface of your computer. For example, Quick Config configures a listener that accepts connections from every network interface. This is probably not ideal for edge machines that connect to unsecure networks (like the Internet). In addition, this command only needs to be run once.

WSMan-Commands-2

Quickly delete the WS-Man listener

Although this command only deletes all WinRM listeners, it effectively turns off any WS-Man communication to a machine.

WSMan-Commands-3

Display your machine’s basic hardware info and operating system properties

Win32_ComputerSystem and Win32_OperatingSystem are common WMI classes and useful for asset information and configuration information as well as some utilization metrics.

WSMan-Commands-4

Output your OS info in XML

XML output makes the data much easier for storing in a database or dealing with programmatically (like with a script). PowerShell makes this that much easier since it works VERY well with XML (see below for a sample PowerShell script).

WSMan-Commands-5

Ping WS-Man on a remote machine and test authorization credentials

Testing WS-Man on a remote machine is very useful in troubleshooting lots of connectivity and configuration issues. When pinging WS-Man without ‘auth’, it allows for the testing of the connectivity and basic service configuration. Using the ‘auth’ parameter tests the necessary authorization. Generally the credentials need to be in the ‘Administrators’ group for ‘auth’ to work. In this case no credentials are provided so the current credentials are used (this can be over-ridden). Notice that the OS version is included when successfully using ‘auth’ to test WS-Man.

WSMan-Commands-6

Grab a remote machine’s WS-Man config

Successfully completing this step pretty much insures that one has complete access to WS-Man on the remote computer.

Grab a remote machine’s CPU load, free memory, and restart a service

WS-Man allows for gathering WMI properties (reading and writing although we’re only reading in this example) as well as invoking methods (starting and stopping a service; as well as rebooting!). Notice the minimal return code for the invoke commands (0 = success).

WSMan-Commands-7

Run a command on a remote machine (this uses winrS, not winrM)

WinRS is another utility that leverages WS-Man. WinRS allows for the execution of local, non-interactive command-line commands on a remote machine and returns the output. In other words, if the command can be run at the CMD prompt without any required input and it only accesses local resource (no network shares for example), then it will most likely work. There are ways to get around the ‘local resource’ issue, but that is out of scope for this blog.

WSMan-Commands-8

Use PowerShell to grab instrumentation via WS-Man

PowerShell is great for consuming data from WS-Man since it works very well with XML. Notice that in the following example it’s easy to ‘surf’ an object in PowerShell. In this case the XML object ‘osInfo’ is displayed on its own, and then expanded to the ‘Win32_OperatingSystem’ branch.

WSMan-Commands-9

A simple PowerShell script

The following script shows how easy it is to automate the collection of WS-Man information using PowerShell.

$machines = ("machine1","machine2","machine3")
foreach ($machine in $machines) {
  [xml]$osInfo = winrm get wmicimv2/Win32_OperatingSystem /format:pretty /r:$machine
  $machine + ": " + $osInfo.Win32_OperatingSystem.LastBootupTime.DateTime
}

More Info

For more information on WS-Man, please see the following articles:

Enjoy!