2013 Scripting Games Event 5

Instructions for the Event 5, the Logfile Labyrinth:

First let’s see how my entry http://scriptinggames.org/entrylist_.php?entryid=1069 performs:

  • Using the 1MB large log files from the zip file in the instructions
  • Using 4GB of files located on my laptop that doesn’t have a SSD disk 😦

Although it wasn’t a requirement, I chose to write a function that could parse many files including large files of 25MB,125MB,..as quick as possible without having a negative impact on memory.

There are many ways to read files content and IIS log files. Here’s a non- exhaustive list I found on the web:

  • Using a SQL DB
  • Using the New-Object -ComObject MSUtil.LogQuery from LogParser
  • Using the built-in Select-String cmdlet
  • Using the built-in switch statement that has a FilePath parameter
  • Using the built-in Get-Content cmdlet that reads lines 1 by 1 by default, or using -ReadCount 0 for all lines, or using a specific number of lines (-Readcount 1000 for example)
  • Using the .Net methods of the class [IO.File]
  • Using the .Net methods associated with the [IO.StreamReader] class
  • Using the [regex] object (my favorite)

All of these methods perform differently, may consume a lot of RAM,…
To select the method, I’ve tested the following

Get-ChildItem -Path .\LogFiles\W3SVC1 | % {             
New-Object -TypeName psobject -Property @{            
    FileName = $_.FullName            
    'FileSize(MB)' = '{0:N2}'-f($_.Length/1MB)            
    GCMethod = (Measure-Command {            
        Get-Content $_.FullName | Out-Null             
    }).ToString()            
    GC1000Method = (Measure-Command {            
        Get-Content $_.FullName -ReadCount 1000 | % { $_ } | Out-Null             
    }).ToString()            
    StreamMethod = (Measure-Command {            
        $reader = new-object System.IO.StreamReader -ArgumentList $_.FullName            
        while ( ($line = $reader.readline()))  {            
        $line            
        }            
    }).ToString()            
}}

Here are a few points that you may notice in my entry.

  • The regular expression in the ValidatePattern attribute will allow IPV4 and IPV6 IP addresses. It means that you can type for example:
    127.0.*
    192.168.*
    *1
    ::1
    2001:*
    fe80*

    I didn’t use the following code because it gave from strange results

    try {            
            [IPAddress]::Parse($_) | Out-Null            
        } catch {            
            throw "An invalid IP address was specified."            
        }

    As I use the -LIKE operator to filter the resulting array of unique IP Addresses, I needed to know whether the Pattern parameter of my function was used so that I can make sure it ends with a wildcard.

    if ($PSBoundParameters.ContainsKey('Pattern')) {            
        if (-not($Pattern.EndsWith("*"))) {            
            $Pattern = "$Pattern*"            
        }            
        Write-Verbose "Using pattern $Pattern to filter the output"            
    }
  • I’ve used the following MSDN page to find out the different file names and understand the different format of IIS log files
  • I’ve also extracted the first four lines in a W3C log file to determine in what column the “c-ip” is located. I used the -TotalCount parameter of the built-in Get-Content for this purpose.
  • You may notice that I also used the new -notin operator of PowerShell version 3.0 to avoid creating a huge array in memory when reading big files containing millions of lines.
  • The total length of my progress bar represents the total size of files. The progress is proportional to the file size.

Here’s my full entry 😎

#Requires -Version 3            
            
Function Get-IPFromIISLog {            
            
[CmdletBinding()]            
Param(            
[Parameter()]            
[ValidatePattern("^(\d|[a-f]|:|\*|\.|\%)*$")]            
[string]$Pattern="*",            
            
[Parameter()]            
[ValidateScript({            
    Test-Path -Path $_ -PathType Container            
})]            
[string]$FilePath = ".\"            
            
)            
Begin {            
            
    if ($PSBoundParameters.ContainsKey('Pattern')) {            
        if (-not($Pattern.EndsWith("*"))) {            
            $Pattern = "$Pattern*"            
        }            
       Write-Verbose "Using pattern $Pattern to filter the output"            
    }            
            
    Function Get-LineStream {            
    [CmdLetBinding()]            
    Param(            
        [int32]$Index,            
        [string]$Separator,            
        [String]$Path            
    )            
    Begin {            
        $arIP = @()            
    }            
    Process {            
        try {            
            $StreamReader =  New-object System.IO.StreamReader -ArgumentList (Resolve-Path $Path -ErrorAction Stop).Path            
            Write-Verbose "Reading Stream of file $Path"            
            while ( $StreamReader.Peek() -gt -1 )  {            
                $Line = $StreamReader.ReadLine()            
                if ($Line.length -eq 0 -or $Line -match "^#") {            
                    continue            
                }            
                $result = ($Line -split $Separator)[$Index]            
                if ($result -notin $arIP) {            
                    $arIP += $result            
                }            
            }            
            $StreamReader.Close()            
            $arIP            
        } catch {            
            Write-Warning -Message "Failed to read $Path because $($_.Exception.Message)"            
        }            
    }            
    End {}            
    } # end of function            
            
}            
Process {            
                
    try {            
        $allFiles = Get-ChildItem -Path $FilePath -Filter *.LOG -Recurse -ErrorAction Stop            
    } catch {            
        Write-Warning -Message "Failed to enumerate files under $FilePath because $($_.Exception.Message)"            
        break            
    }            
            
    if ($allFiles) {            
        $IPCollected = @()            
        $Count = 1            
        $FileSizeSum = 0            
        $TotalSize = (($allFiles | ForEach-Object { $_.Length }) | Measure-Object -Sum).Sum            
        $allFiles | ForEach-Object {            
            $File = $_            
            $FileSizeSum += $File.length            
            $WPHT = @{            
                Activity = "Reading file $($File.Name) of size $('{0:N2}'-f ($File.Length/1MB))MB" ;            
                Status = '{0} over {1}' -f $Count,($allFiles).Count ;            
                PercentComplete  = ($FileSizeSum/$TotalSize*100) ;            
            }            
            Write-Progress @WPHT            
            $Count++            
            
            # Based on the file name we know the IIS Format            
            Switch -Regex ($File.Name) {            
                '^u_ex.*\.log'  { $IISLogFormat = 'W3C'    ; break}            
                '^ex.*\.log'    { $IISLogFormat = 'W3C'    ; break}            
                '^in.*\.log'    { $IISLogFormat = 'IIS'    ; break}            
                '^nc.*\.log'    { $IISLogFormat = 'NCSA'   ; break}            
                default         { $IISLogFormat = 'Custom' ; break}            
            }            
            Switch ($IISLogFormat) {            
                'W3C' {            
                    Write-Verbose "Reading W3C formatted file $($File.FullName)"            
                    try {            
                        $First4Lines = Get-Content -Path $($File.FullName) -TotalCount 4 -ErrorAction Stop            
                    } catch {            
                        Write-Warning "Failed to read the content of the file $($File.Name) because $($_.Exception.Message)"            
                    }            
                    if ($First4Lines) {            
                        $i = -1            
                        $Index = ($First4Lines[-1] -split "\s" | ForEach-Object {            
                            [PSObject]@{ Index=$i ; FieldName = $_}            
                            $i++            
                        } | Where-Object { $_.FieldName -eq "c-ip"}).Index                                    
                    }            
                    if ($Index) {            
                        [array]$IPCollected += (Get-LineStream -Path $File.FullName -Separator "\s" -Index $Index)            
                    } else {            
                        Write-Warning "Could not find the c-ip field in the W3C log file $($File.FullName)"            
                    }            
                }            
                IIS {            
                    Write-Verbose "Reading IIS formatted file $($File.FullName)"            
                    [array]$IPCollected += (Get-LineStream -Path $File.FullName -Index 0 -Separator ",")            
                }            
                NCSA {            
                    Write-Verbose "Reading NCSA formatted file $($File.FullName)"            
                    [array]$IPCollected += (Get-LineStream -Path $File.FullName -Index 0 -Separator "\s")            
                }            
                default {            
                    Write-Warning "Cannot parse a custom log file $($File.FullName)"            
                }            
            }            
            $IPCollected = ($IPCollected | Sort -Unique)            
        }            
        Write-Verbose ("A total of {0} unique IP were collected" -f $($IPCollected.Count))            
        $IPCollected | Where-Object { $_ -like $Pattern }            
    } else {            
        Write-Warning "No file with .LOG extension found in this folder and subtree"            
    }            
}            
End {}            
}            

2013 Scripting Games Event 4

Instructions:

  • Step 1: Scoping the event
  • The requirements don’t tell:

      • what operating system will be used to run the code
      • what version of powershell will be used to execute the code
      • whether the Active Directory module is present and loaded
      • what version of Windows Domain Controllers are running
      • how the Active Directory is configured: replication,…

    I made 3 assumptions for this event:

      • We won’t use PowerShell V1 because Get-Random cmdlet isn’t available. Writing a piece of code to simulate the behavior of Get-Random would have been interesting but was totally out-of-scope for this event. Anyway this doesn’t mean that we cannot come up with clever ways of selecting 20 pseudo randomly users.
      • If the domain admin that will run the code has installed Active Directory modules for Powershell (using RSAT) and still has only Active Directory running on Windows 2003, he has also taken care of installing Active Directory Management Gateway Service. The domain admin will not run the code on a broken plateform.
      • The instructions say “A Domain Admin will always run the command”. It doesn’t tell if he will run the code from a computer that doesn’t belong to Active Directory. I assumed he will select a machine (client or server) that belongs to an Active Directory because that’s just common sense. Checking that the computer is a member of a domain and not a workgroup isn’t a requirement.
  • Step 2 Gathering information on Active Directory properties
  • One of the most difficult tasks in this event is that there are many ways to query Active Directory, some techniques return the raw value and some display a nicely formatted value. They don’t have the same properties name and count returned by default.

    The Get-ADUser cmdlet returns by default 10 properties. Using core cmdlets like Get-Item on a user object while navigating the AD drive will return only 4 properties by default.
    Get-ADUser will display a nicely formatted property PasswordLastSet that is derived from the 64bit integer value named pwdLastSet…

    To make it even more difficult, you should find out how accurrate these properties are, whether they are replicated across all domain controllers, how active directory stores them and how some of these properties are calculated. One of the most common mistake is to use the LastLogon property. This property isn’t replicated across DCs, it’s thus unreliable. The lastlogontimestamp is replicated but it’s not 100% accurate.

    To illustrate that and to get more information about Active Directory, I’ve got some really nice links to share from the following sources:

  • Step 3: Organize scripting tasks
    • Testing whether the Active Directory module is present or not
    • To be absolutely sure that the orginal module from Microsoft is present and being used, I first remove any module named “ActiveDirectory”.

      Remove-Module -Name "ActiveDirectory" -Force -ErrorAction Stop

      Then I try to load the original module from Microsoft. As I don’t know if the $env:PSModulePath has been modified, I try to load the module from its original path. Yes, the Import-Module cmdlet allows filepath as input.

      Get-Item "$env:systemroot\system32\WindowsPowerShell\v1.0\Modules\ActiveDirectory" -EA Stop |            
      Import-Module -ErrorAction Stop            
      
    • Testing whether if the Active Directory drive is mounted or not
    • For fun, I decided to use the core cmdlets (Get-ChildItem, Get-Item,…) to query the AD: drive. I needed to know whether the drive was mounted along with the module import or disabled on purpose using the $Env:ADPS_LoadDefaultDrive variable:

      if (Test-Path Env:ADPS_LoadDefaultDrive) {             
          if ([bool]($Env:ADPS_LoadDefaultDrive)) {            
              # The drive is loaded because the value is not 0            
              $ADDriveLoaded = $true            
              Write-Verbose "AD drive mounted by default using environment variable"            
          } else {            
              # $Env:ADPS_LoadDefaultDrive = 0 -> don't load            
              Write-Verbose "AD drive mounting was disabled using environment variable"            
              if ($Fun) {            
                  try {            
                      # Mount the drive using a Global Catalog            
                      New-PSDrive -PSProvider ActiveDirectory -Name AD -Root "" -server (            
                          Get-ADDomainController -Discover -Service 2 -ErrorAction Stop            
                      ).Name -ErrorAction Stop            
                      $ADDriveLoaded = $true            
                      Write-Verbose "AD drive mounted in current scope"            
                  } catch {            
                      $ADDriveLoaded = $false            
                      Write-Warning -Message "Failed to mount AD drive in current scope because  $($_.Exception.Message)"            
                  }            
              } else {            
                  $ADDriveLoaded = $false            
              }            
          }            
      } else {            
          # The AD drive is loaded by default            
          $ADDriveLoaded = $true            
          Write-Verbose "AD drive mounted by default"            
      }
    • Avoid the recursive search gotcha
    • I have noticed during the testing phase, that the Active Directory provider no longer allow a Recursive search in Powershell 3.0 as it did in Powershell 2.0
      I had to add a mandatory path to an OU to workaround that issue.

      if ($Fun -and $ADDriveLoaded) {            
          Write-Verbose "Using the Active Directory module in fun mode"            
          Switch ($PSVersionTable.PSVersion.Major) {            
              2 {            
                  $GCIHT = @{            
                      Path = "AD:\$Path"            
                      Recurse = $true            
                  }            
                  break            
              }            
              default {            
                  $GCIHT = @{Path = "AD:\$Path"}            
              }            
          }            
      $allusers = @(Get-ChildItem -Filter "(&(objectClass=user)(objectCategory=person))" @GCIHT)            
      
    • Validating an OU path
    • I could not validate the path of the OU in the parameter block as I needed to know whether the AD drive was mounted or not. I added the following in the Begin block:

      # Validate the OU            
      if ($Fun -and $ADDriveLoaded) {            
          if (-not(Test-Path -Path "AD:\$($Path)" -PathType Container)) {            
              Write-Warning "Invalid OU path provided"            
              break            
          }            
      }
    • Solving the case of the User-Account-Control attribute and other Active Directory attibutes
    • Can the userAccountControl attribute be used to determine if the user account is locked or if his password is expired?
      The KB article kb305144 says that:

      Here’s also a comment on the MSDN page about the userAccountControl

      Thanks to the Powershell magazine PSTip I have seen that the msDS-User-Account-Control-Computed AD attribute is a constructed attribute

      Thanks to Richard Mueller and his wiki page we also know how these attributes are calculated:








      As you can see, this wiki page is just awsome, it actually answers many questions 😎

      When using ADSI or the AD: drive, I’ve used this kind of code to display nicely formatted properties:

      1..$MaxUsers | ForEach-Object {            
          $i = (Get-Random -Maximum $MaxRandom  -Minimum 0)            
          $propsar= 'sAMAccountName','department','title','userAccountControl','lastLogonTimestamp',            
                      'pwdLastSet','msDS-User-Account-Control-Computed'            
          $user = Get-Item -Path "AD:\$($allusers[$i].distinguishedName)" -Properties $propsar            
          New-Object -TypeName PSObject -Property @{            
              UserName = $user.sAMAccountName            
              Enabled = -not($user.userAccountControl -band 2)            
              Department = $user.department            
              Title = $user.title            
              LastLogonDate = switch ($user.lastLogonTimestamp) {            
                  $null {            
                      'Never' ; break            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
              LockedOut = [bool]($user.'msDS-User-Account-Control-Computed' -band 16)            
              PasswordExpired = [bool]($user.'msDS-User-Account-Control-Computed'-band 8388608)            
              PasswordLastSet = switch ($user.pwdLastSet) {            
                  0 {            
                          if ($user.userAccountControl -band 65536) {            
                              0 ; break            
                          } else {            
                              'Must Change password at next logon' ; break            
                          }            
                  }            
                  default {            
                      [datetime]::FromFileTime([int64]::Parse($_))            
                  }            
              }            
          }            
      } |             
      Select -Property UserName,Enabled,Department,Title,LastLogonDate,LockedOut,PasswordExpired,PasswordLastSet |            
      Sort -Descending -Property PasswordLastSet

      Having the PwdLastSet attribute set to 0 doesn’t necessarily means that the user must change his password at next logon
      (source)

    • Making sure to get results quickly
    • Performance wasn’t a requirement. But I didn’t know how many user objects are present in Active Directory. We could have 1000, 10 000 or even more. To get results more quickly, I added a range to the MaxUsers parameter and used its upper limit as a value for the ResultSetSize parameter of the Get-ADUser cmdlet.

      [parameter()]            
      [ValidateRange(0,1000)]            
      [int]$MaxUsers=20
      $allusers = ActiveDirectory\Get-ADUser -Filter * -ResultSetSize 1000

      ADSI has also a way to set the maximum number of objects returned.

      $Searcher = [System.DirectoryServices.DirectorySearcher]$Root            
      $Searcher.SizeLimit = 1000

      Unfortunately, we cannot instruct core cmdlets to get a certain number of items. As I discovered the issue with the recursive search and made the OU path a mandatory parameter, I could get 0 or more AD users objects. Yes, I could get 0 object if you type a valid OU path where there isn’t any user object. Worse, I noticed another problem when there’s only 1 user object. My Get-Random cmdlet was throwing an error because you can’t do:

      Get-Random -Maximum 1 -Minimum 1

    • (Re)using code to produce the HTML page
    • In the previous event, I used the awsome work of Don Jones who wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/

    • Working on parameter validation
    • The instruction said that:

      Get PowerShell to do as much of that validation as possible – Dr.Scripto has to review your code, and doesn’t want to read a lot of manual validation
      script.

      I spent some time testing and reading the help about ValidateScript.
      I think I found a way to put as much as validation as possible in the ValidateScript attribute, keep it easy to read, grasp the logic and of course maintain.
      The only thing I wanted was to make the error behave as if it was thrown by a cmdlet.
      I wanted to avoid the full ValidateScript content to be displayed in the console when an error occurs because having too many lines could be somehow “counter-productive”. I’m also not a big fan of using the throw keyword… but sometimes you just need it πŸ˜‰

      [parameter()]            
      [ValidateScript({            
          # Test that the parent path exists            
          if (-not(Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container)) {            
              throw New-Object System.ArgumentException -ArgumentList "Filepath doesn't exist"            
          }            
          # Validate the filename extension            
          if ( (Split-Path -Path $_ -Leaf) -notmatch ".*\.html?$") {            
              throw New-Object System.ArgumentException -ArgumentList "File extension must end by .html or .htm"            
          }            
          return $true            
      })]            
      [string]$FilePath = "$Env:TEMP\audit.report.html"

      And here’s the result

      Not bad? What do you think?

    Final word
    This event was really hard but also fun. It was a good opportunity to learn many many things about Active Directory, like ADSI,…
    I’m not super proud of how my entry looks like but I know it does the job.
    You can have a look at it: http://scriptinggames.org/entrylist_.php?entryid=914
    If you see anything that’s wrong or that I can improve, I welcome your feedback. If you find something you don’t understand, leave a comment on this blog post, I’ll try to explain it.

    2013 Scripting Games Event 3

    As much as I like feedback on my Scripting Games entries, I still see voters using the BOFH mode.

    I’m sure my entry http://scriptinggames.org/entrylist.php?entryid=729 shouldn’t be rated with 1 star according to the scoring guidelines that you absolutely need to read before voting.

    Really, you’d fire me? Did I fail?
    I think you should see what it looks like as sometimes a picture is worth a thousand words:

    First I’ve had a look at the built-in cmdlet Get-Volume that comes with the Storage module in Windows 8 / Windows 2012 and that has already a pretty output in the console:

    But, this cmdlet cannot target older version of Windows because it queries a WMI namespace and class that only exists on Windows 8 based operating systems.

    Get-Content Function:\Get-Volume


    This cmdlet is great but not appropriate for the scenario.

    Then, I started the traditional way. I created a header, added some HTML data…

    $HTMLheader = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title>DRIVE FREESPACE REPORT</title>
    </head><body>
    
    "@
    

    There’s nothing wrong with this approach. It will work but it’s a bit cumbersome.

    Then I remembered that Don Jones wrote a free ebook named “Creating HTML Reports in PowerShell” that you can download on this page: http://powershell.org/wp/newsletter/

    I’ve just copied and used the awsome function he wrote. He already did the hard work, why would I reinvent the wheel? 😎

    The only thing I changed is the colors in the report. I extracted the blue colors from Excel with MSPaint and translated RGB colors to Hexadecimal with this website: http://www.rgbtohex.net/hextorgb/

    I also didnt’ know how to create an horizontal bar in HTML. I googled the “horizontal bar Html” keywords and found the answer on this page: http://www.w3schools.com/tags/tag_hr.asp

    Mike F Robbins won the Advanced Event2 2nd with this entry http://scriptinggames.org/entrylist_.php?entryid=552 where he demonstrated how to use the CIM (Common Information Model) to query computers over the WSMAN protocol or failing over DCOM for older versions of Windows.

    I liked his approach but I also like sometimes a very straightforward approach because it would make a difference, a boost on performance when you query thousands of computers/servers.
    I’ve seen many entries that use first the Test-Connection cmdlet before using the Get-WmiObject cmdlet to retrieve data from remote computers. They actually probably don’t realize that Test-Connection uses WMI behind the scene. It even has a -Credential parameter. Here’s what the NOTES in the help of the Test-Connection cmdlet say:
    . The more WMI classes you query, the longer it will last.

    You may wonder why I remove Credentials when creating a CIM session over WSMAN that targets the localhost. It isn’t really required. Well, I just remove them because my vanilla Windows 8 computer has WinRM turned off by default 😦

    So, my PROCESS block has 2 parts. I first try to establish a CIM session over WSMAN with the remote computer and then failover DCOM if WSMAN doesn’t work. I collect all the successfully opened CIM sessions into an array. When writing this code I wondered whether CIM sessions are throttled like PSSessions. The Get-Volume cmdlet has a -ThrottleLimit parameter whereas the New-CIMSession and New-CimSessionOption cmdlets don’t.

    In the second part of the PROCESS block, I query the Win32_Volume WMI class using the -Filter parameter of the Get-CIMinstance cmdlet.
    Why Win32_Volume ? Well, it’s more exhaustive than Win32_LogicalDisk. The Win32_Volume WMI class will list the system reserved partition, a NTFS hidden volume (see above image of the HTML report). It will also list local disks that have been mounted (mountpoints, in other words) as well as mounted VHD drives. My C: drive is actually a VHD located on the F: drive. Boe Prox wrote a nice blog post on mountpoints that you can find here.

    The other “cool” thing in this second part is that I’m using some of the properties of the CIM session object to display the target computer name (the one passed as parameter to the function) and the protocol being used to communicate with the remote computer.

    I also extracted the real NetBIOS name of the computer from the CIM data retreived that the person who rated my script with one star didn’t notice or understand 😦 I forgive you, I’m using some of the new Powershell Version 3.0 syntax enhancements πŸ˜€

    $NetBiosName = $CimData.SystemName | Select-Object -Unique

    Mike uses the following classic code to achieve the same thing from WMI data:

    $MachineName = $DiskSpace | Select-Object -ExpandProperty SystemName -Unique

    See his approach for Event3: http://mikefrobbins.com/2013/05/16/2013-powershell-scripting-games-advanced-event-3-bringing-bad-habits-from-the-gui-to-powershell/

    2013 Scripting Games Event 2

    As I write this post, Event 2 winners have been announced. Event 3 voting period started and is opened till 2013-05-21 00:00:00 (GMT). In other words, we just passed mid-point in Scripting Games 2013

    My entry for Event 2 in Advanced category was awarded place #5 by the voters’ community.

    … and it was ranked #1 by the expert judges and the mighty panel of celebrity judges

    I’d like to thank first all the anonymous voters on my entry, those who left a comment on my entry (it’s important to get feedback).
    I’d like to address a special thank to the judges. As I’m also a voter, I understand now how difficult and time consuming it may be to appreciate others’ work and leave a comment that help others improve their PowerShell scripting skills.

    My entry for Event 2 in the Advanced category won: http://scriptinggames.org/entrylist.php?entryid=465
    I’m sure it didn’t win because it was superior to others, I’ve seen some greats entries and learned many things by looking, voting and commenting these entries.
    I believe it won because there are probably some great learning points in it (hopefully, that’s what you’ll find).
    I actually wrote this entry twice.
    When I wrote my first version, I wanted to keep it simple. I started by the beginner entry instructions, wrote and validated some commands.
    Then I changed it to make it an advanced function. As I was using the Win32_ComputerSystem WMI class, I also read the following limitations on this MSDN page:

    I’ve left the code 2 days and before testing it on various systems, I read the instructions again. I didn’t pay enough attention the first time. I missed the “reliable” word.
    We were supposed to find reliable info whatever the operating system. I googled the following keywords “WMI list sockets” and found this page
    I had a look at these two knowledgebase articles:

    After reading the “more information” section of these KB articles, the word “reliable” in the instructions made fully sense.
    I decided to test every versions of Windows, check if the KB is present for Windows 2003 and XP… I actually even used the word “reliable” as a variable name to make sure the logic in the script is easier to follow.
    I still wanted to keep it simple and easy to read. That’s why I implemented a huge switch block for all the versions of Windows.
    What’s cool with this $Reliable variable is that it has 2 types depending on the OS version, either a boolean or an array of WMI object with 1 element or none.

    The $Reliable could be $null if the specific KB isn’t found by querying the Win32_QuickFixEngineering WMI class.
    The $Reliable could be hold 1 System.Management.ManagementObject#\Win32_QuickFixEngineering object if the specific KB is retreived from WMI.
    This trick allowed me to just use a single if statement block.

    if ($Reliable) {            
        Write-Verbose -Message "Reliable set to true"            
        $CS = Get-WmiObject -Class Win32_ComputerSystem  -Property $CSprops @WMIHT            
        $ProcessorsCount = $CS.NumberOfProcessors            
        $CoresCount      = $CS.NumberOfLogicalProcessors            
    } else {            
        Write-Verbose -Message "Reliable set to false"            
        $CS = Get-WmiObject -Class Win32_ComputerSystem  -Property "TotalPhysicalMemory","NumberOfProcessors" @WMIHT            
        $CoresCount      = $CS.NumberOfProcessors            
        $ProcessorsCount = 0            
    }

    To make sure to understand how this work, you can test the following in your console:

    if ($true) { $true } else { $false }            
                
    if (0) { $true } else { $false }            
                
    if (1) { $true } else { $false }            
                
    # test an empty array            
    if (@()) { $true } else { $false }            
                
    # test an array with 1 element            
    if (@('1')) { $true } else { $false }


    As you can see in the above image, Mike F Robbins and Francois-Xavier Cat, the top 2 crowdscores on Advanced Event 2, already wrote blog articles that you can find here:

    You can also read and learn many things from the Expert judges:

    or by other competitors blogging on Scriptings Games that Mike started referencing on this landing page:

    In case you missed it, a motivating prizes list is available on this page: http://powershell.org/wp/2013/04/17/scripting-games-2013-prize-list/

    Quick follow-up on WSUS on Windows Server 2012 Core from scratch

    I’ve been using WSUS from the Powershell console since I published my post on WSUS on Windows Server 2012 Core from scratch in February 2013.

    I’d like to mention two mistakes I did:

    • I don’t really need to decline updates when they have approved. They can be also “not approved” which is their original state.
    • My filter for Office 2010 was a little bit too restrictive.

    Let’s see how I manage my updates for Windows 7 and do not reproduce the above mistakes

  • Start a synchronization
  • # Check the date of the last sync (it's configured to sync manually)            
    (Get-WsusServer).GetSubscription().GetLastSynchronizationInfo()            
                
    # Initiate a sync            
    (Get-WsusServer).GetSubscription().StartSynchronization()            
                
    # View its progress            
    (Get-WsusServer).GetSubscription().GetSynchronizationStatus()            
                
    # Check that the last sync date is today and a success            
    (Get-WsusServer).GetSubscription().GetLastSynchronizationInfo()
  • Handle Windows 7 security updates
  • # Find any update that has 'Windows 7' string mentioned            
    $allW7updates = (Get-WsusServer).SearchUpdates("Windows 7")            
                
    # Select my target group of computers            
    $targetgroup = (Get-WsusServer).GetComputerTargetGroups() |             
    Where Name -eq "Windows 7 x64"            
                
    # View all the x64 updates for Windows 7            
    $allW7updates | Where { (-not($_.IsSuperseded)) -and            
     ($_.Title -match "x64") -and             
     ($_.UpdateClassificationTitle -eq "Security Updates") -and            
     (-not($_.isApproved)) -and            
     (-not($_.isDeclined))             
    } |             
    ft Title,State,KnowledgebaseArticles,SecurityBulletins -AutoSize            
                
    # Mark these required updates as 'to be installed' by client computers            
    $allW7updates | Where { (-not($_.IsSuperseded)) -and            
     ($_.Title -match "x64") -and            
     ($_.UpdateClassificationTitle -eq "Security Updates") -and            
     (-not($_.isApproved)) -and            
     (-not($_.isDeclined))} |            
    Where {            
     $_.Title -notmatch ".NET Framework 4"            
    } | ForEach-Object -Process {            
     $_.Approve(            
      [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,            
      $targetgroup            
     )            
    }            
                
    # View superseded updates that were previously approved            
    $allW7updates | Where {            
     ($_.IsSuperseded) -and            
     ($_.isApproved)            
    } | ft Title,SecurityBulletins            
                
    # Remove them by flagging them as 'not approved'            
    $allW7updates |             
     Where { ($_.IsSuperseded) -and ($_.isApproved) } |            
     ForEach-Object -Process {            
      $_.Approve(            
       [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::NotApproved,            
       $targetgroup            
      )            
     }
  • Handle Office 2010 updates
  • # View all Office 2010 (32bit) updates            
    (Get-WsusServer).SearchUpdates("2010") | Where {            
     (-not($_.IsSuperseded)) -and            
     ($_.Title -match "32-bit") -and            
     ($_.UpdateClassificationTitle -eq "Security Updates") -and            
     (-not($_.isApproved)) -and            
     (-not($_.isDeclined))             
    } | ft Title,SecurityBulletins,IsApproved,IsSuperseded -AutoSize            
                
    # Mark these required updates as 'to be installed' by client computers            
    (Get-WsusServer).SearchUpdates("2010") | Where {            
     (-not($_.IsSuperseded)) -and            
     ($_.Title -match "32-bit") -and            
     ($_.UpdateClassificationTitle -eq "Security Updates") -and            
     (-not($_.isApproved)) -and            
     (-not($_.isDeclined))             
    } | ForEach-Object -Process {            
     $_.Approve(            
      [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,            
      $targetgroup            
     )            
    }            
                
    # View superseded updates that were previously approved            
    (Get-WsusServer).SearchUpdates("2010") | Where {            
     ($_.IsSuperseded) -and            
     ($_.isApproved)             
    } |             
    ft Title,SecurityBulletins,IsApproved,IsSuperseded -AutoSize            
                
    # Remove them by flagging them as 'not approved'            
    (Get-WsusServer).SearchUpdates("2010") | Where {            
     ($_.IsSuperseded) -and             
     ($_.isApproved) } |             
    ForEach-Object -Process {            
     $_.Approve(            
      [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::NotApproved,            
      $targetgroup            
     )            
    }

    Follow-up on monitoring the Scripting Games 2013 leaderboard

    I know I should spend more time polishing my Scripting Games entries but there’s also so much fun coding a way to monitor the leaderboard πŸ˜›
    Since my previous post many things have changed. The online page displays only the top 10 users in each track. It means I cannot get your current position if the event isn’t closed (voting included) unless you appear in the top 10 users of the ongoing event.

    When I wrote my previous entry, I took the wrong approach to find your name. Select-String will find all matches by default. Now that there are two events, if your name appears in both top 10 users, I got 2 matches. Of course, I could have used Select-String with the -Context parameter but it will capture the specified number of lines before and after the line with the match:

    $page | sls -Pattern "Advanced 2" -Context 10


    At the end, I didn’t use Select-String as I’ve found a quicker way to extract the top 10 users from the online page with a while loop 😎

    I’ve updated the script. The Get-PSH2013Leaderboard function’s features remain unchanged. I’ve created 2 “child” functions that will either read the top 10 users online or download and read all closed entries in your temp directory:

    Join-Path -Path $env:temp -ChildPath "SG2013"

    To get things run quicker and as voting ended, it’s downloaded only once.

    To use the version 2.0 of the Scripting Games 2013 monitoring script, I load all the functions by dot-sourcing the main script like this:

    . E:\Monitor-SG2013.ps1


    Now,
    For event 1: I can see the top 5 users and my own rank. The total number of votes property was available and I’ve added it.
    If it’s the first time I use it, I will see the following progress bar indicating that entries are downloaded in the %temp%\SG2013 directory:

    If I run the same command again, I get results more quickly as entries are being read locally.

    Get-PSH2013Learder -Category Advanced -EventNumber 1 -TopUsers 5 -UserName "_Emin_" |            
    Select Rank,Name,Score,Votes |            
    ft -AutoSize

    For event 2: as long as it isn’t closed, it makes sense to query the top 10 users online.

    Get-PSH2013Learder -Category Advanced -EventNumber 2 -TopUsers 5 -UserName "_Emin_"  -Repeat

    As you can see in the above image, I got only 16 votes on my first entry whereas Mike F. Robbins got 74. Sure, Mike has a fan club πŸ˜€ and I don’t 😦

    In the meantime, scoring guides lines have been published by Don Jones: http://powershell.org/wp/the-scripting-games/scoring-guidelines/ and a nice reminder apprears in the standard voting mode:
    …as well as in Beta voting mode:

    If you read this post that far and before voting ends on event 2 (2013-05-14 00:00:00 (all times GMT)), you actually owe me a vote on my entry (if you didn’t rate it yet): http://scriptinggames.org/entrylist.php?entryid=465. Don’t forget to write a comment on what you like or don’t like. The whole point of the Scripting Games is to get feeback on my work and have fun.

    Sure, you voted on my entry so that I can beat Mike? πŸ˜‰ Here’s the link to the version 2.0 of Monitor-SG2013.ps1

    Monitor the Scripting Games 2013 leaderboard

    Last year for my first participation in the Scripting Games, I used a brilliant script Watch-SG2011LeaderBoard written by Bartek Bielawski, the winner in the 2011 advanced category. I just fixed the URL of the 2012 scripting games and a few more things that I cannot remember. This script allowed me to see the top users and my own score.

    But this year, the leaderboard isn’t hosted on poshcode.org, there are only 6 events and everybody can vote and comment scripts.
    I decided to write a function from scratch that will get the leaderboard of a given category, that can include your username and score whatever your position and can refresh the top users list every 10 minutes by default.

    Let’s see the ScriptingWife‘s rank for event 1 in the Beginner category

    Get-PSH2013Learder -Track Beginner -Event 1 -User "ScriptingWife"

    Let’s see the top 5 users in the Advanced category

    Get-PSH2013Learder -Category Advanced -EventNumber 1 -TopUsers 5|            
    Select Rank,Name,Score | ft -AutoSize

    Now let’s see how much I (failed?|got appreciated?) 😦

    Get-PSH2013Learder -Cat Advanced -Event 1 -User "_Emin_" -EveryMinutes 60 -Repeat

    Enjoy the following code 😎 and if you really do, don’t forget to vote for me on event 1 πŸ˜‰ using this link: http://scriptinggames.org/entrylist.php?entryid=219

    #Requires -Version 3            
    Function Get-PSH2013Learder {            
    <#
        .SYNOPSIS
            Get the Scripting Games 2013 leaderboard of a given category
       
        .DESCRIPTION
            Just for fun.
        
        .PARAMETER Category 
            The full name of the category you're looking for.
        
        .PARAMETER EventNumber
            Integer that represents the event number.        
        
        .PARAMETER Repeat
            Switch to go in an infinite loop.
    
        .PARAMETER EveryMinutes
            When executed in loop mode, it allows you to set how often the display is "refreshed".
            By default, it refreshes every 10 minutes.
        
        .PARAMETER UserName
            Your full user name. It will be added to the Top users list and show your rank and how much you scored.
        
        .PARAMETER TopUsers
            Integer that allows you how many top users are included in the output.
        
        .EXAMPLE
            Get-PSH2013Learder -Category Beginner -EventNumber 1 -UserName "ScriptingWife"
            Retrieve the top 10 users in the Beginner category for Event 1 and include ScriptingWife regardless her current position
        
        .EXAMPLE
            Get-PSH2013Learder -Category Advanced -EventNumber 1 -TopUsers 5| Select Rank,Name,Score | ft -AutoSize
            Retrieve the top 5 users in the Advanced category
    
        .EXAMPLE
            Get-PSH2013Learder -Category Advanced -EventNumber 1 -UserName "_Emin_" -EveryMinutes 60 -Repeat
            Retrieve the Top 10 users in the Advanced category, include my username and refresh the list every hour
    #>            
                
    [CmdletBinding(DefaultParameterSetName='All')]            
    Param(            
        [Parameter(Mandatory)]            
        [Alias('Track')]            
        [ValidateSet('Advanced','Beginner')]            
        [string]$Category,            
                
        [Parameter(Mandatory)]            
        [ValidateRange(1,6)]            
        [int]$EventNumber,            
                
        [Parameter(ParameterSetName='Loop',Mandatory)]            
        [switch]$Repeat,            
                    
        [Parameter(ParameterSetName='Loop')]            
        [ValidateRange(1,1140)]            
        [int]$EveryMinutes=10,            
                
        [Parameter()]            
        [string]$UserName,            
                
        [Parameter()]            
        [ValidateRange(1,90)]            
        [Int]$TopUsers = 10            
    )            
    Begin{            
        # Get the page             
        try {            
            $URI = 'http://scriptinggames.org/serverside-leaderboard.php'            
            $page = (Invoke-WebRequest -Uri $URI -ErrorAction Stop).AllElements[0].OuterText -split "`n"            
        } catch {            
            Write-Warning "Failed to read the web page"            
            break            
        }            
        # Calculate the line number where the caterory keyword is located            
        $StartingLine = ($page | Select-String -Pattern "^$($Category)\s[$EventNumber]").LineNumber            
        Write-Verbose "Found category $Category at line $StartingLine"            
    }            
    Process {            
        if ($PSCmdlet.ParameterSetName -eq 'Loop') {            
                
            Write-Verbose -Message ('Execution time: {0}' -f (Get-Date).ToLongTimeString()) -Verbose            
                
            # Call the function once, i.e., without the EveryMinutes parameters            
            $PSBoundParameters.Remove('EveryMinutes') | Out-Null            
            $PSBoundParameters.Remove('Repeat') | Out-Null            
            Get-PSH2013Learder @PSBoundParameters            
                
            # Wait            
            Write-Verbose -Message ('Waiting {0} minutes, next refresh at {1}' -f $EveryMinutes,(Get-Date).AddMinutes($EveryMinutes).ToLongTimeString()) -Verbose            
            Start-Sleep -Seconds ($EveryMinutes*60)            
            Clear-Host            
                
            # Call the function recursively             
            Get-PSH2013Learder @PSBoundParameters -Repeat:$true -EveryMinutes $EveryMinutes            
                
        } else {            
            $i = 1            
            @($page)[$StartingLine..($StartingLine+$TopUsers-1)] | ForEach-Object -Process {            
                New-Object -TypeName PSObject -Property @{            
                    Rank = $i            
                    Name =  (($_ -split ",")[0] -replace ([char]8226),"").Trim()            
                    Score = ($_ -split ", scoring")[1].Trim()            
                }            
                $i++            
            }            
            if ($UserName) {            
                try {            
                     $UserLine = ($page | Select-String -Pattern "^$([char]8226)\s$($UserName),\sscoring" -ErrorAction Stop)            
                } catch {            
                    Write-Warning "Failed to find username $UserName, type the full username"            
                }            
                if ($UserLine) {            
                    $UseratLine = ($UserLine).LineNumber            
                    New-Object -TypeName PSObject -Property @{            
                        Rank = $UseratLine - $StartingLine            
                        Name =  $UserName            
                        Score = (($page | Select-String -Pattern "^$([char]8226)\s$($UserName),\sscoring").Line -split ", scoring")[1].Trim()            
                    }            
                } else {            
                    Write-Warning "Exact Username $UserName not found"            
                }            
            }            
        }            
    }            
    End {}            
    }