Mick's IT Blogs

Mick's IT Blogs

Latest Updates

26 July 2017

Dell BIOS Reporting Tool

Posted By: Mick Pletcher - 10:32 AM















Recently, we ran into a problem when we discovered some of the newer laptops were not automatically disabling the WiFi when connected to ethernet. What made the task even more difficult was that all of our Dell Latitude 7480 systems were already deployed. Being in the legal industry, it is more difficult to ask for time to troubleshoot problems when attorneys bill by the hour.

We knew there was either a new BIOS setting for the 7480 or it had been taken away. To get a list of all the BIOS settings for the 7480, I wrote the script below that uses the Dell Command | Configure to get the BIOS options, settings, and descriptions. You can use the Dell Command | Configure GUI application, but that also requires getting time on the remote machine. This script will grab the info in the background without any interruption to the user.

The script first gets a list of all the available BIOS settings and filters out the following items since I did not see the need for these in the reports:


  • help
  • version
  • infile
  • logfile
  • outfile
  • ovrwrt
  • setuppwd
  • sysdefaults
  • syspwd
The next thing it does it to grab the set value for each setting and then it retrieves the description of the setting. The script formats this data into a table that is exported to a .CSV file for easy viewing. In future models, there will likely be new data, so the script will likely need to be updated. There may also be some data the script did not have access to as the firm I work at only has 8 models of Dell systems. 

The first thing you need to do is to get a list of all systems with their BIOS version. You will want to run this in SCCM in order to find the systems with the latest BIOS version to generate the report on. Here is the WQL code for performing a query in SCCM. 

 select SMS_G_System_COMPUTER_SYSTEM.Manufacturer, SMS_G_System_COMPUTER_SYSTEM.Model, SMS_G_System_PC_BIOS.SMBIOSBIOSVersion, SMS_R_System.Name from SMS_R_System inner join SMS_G_System_PC_BIOS on SMS_G_System_PC_BIOS.ResourceID = SMS_R_System.ResourceId inner join SMS_G_System_COMPUTER_SYSTEM on SMS_G_System_COMPUTER_SYSTEM.ResourceId = SMS_R_System.ResourceId order by SMS_G_System_COMPUTER_SYSTEM.Manufacturer, SMS_G_System_PC_BIOS.SMBIOSBIOSVersion, SMS_G_System_COMPUTER_SYSTEM.Model  

Once you get a list of the systems and choose which one to execute the script on, you have some options. You could either deploy the script through SCCM or you could execute it remotely using PSEXEC. Personally, I used PSEXEC. The only parameter you will need to define is the FilePath, which is the location where the .CSV will be written to.

Here is an example of a .CSV file I ran on my own machine. Some values are left blank because the output exceeded a reasonable amount for this spreadsheet, such as hddinfo. Some are also blank due to security, such as hddpwd.



You can download the script from my GitHub repository located here.


 <#  
      .SYNOPSIS  
           BIOS Reporting Tool  
        
      .DESCRIPTION  
           This script will query the BIOS of Dell machines using the Dell Command | Configure to report the data to SCCM via WMI.  
        
      .PARAMETER FilePath  
           UNC path where to write the file output  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.141  
           Created on:       7/18/2017 9:31 AM  
           Created by:       Mick Pletcher  
           Filename:         DellBIOSReportingTool.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$FilePath  
 )  
   
 function Get-Architecture {  
 <#  
      .SYNOPSIS  
           Get-Architecture  
        
      .DESCRIPTION  
           Returns whether the system architecture is 32-bit or 64-bit  
        
      .EXAMPLE  
           Get-Architecture  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $OSArchitecture = (Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture).OSArchitecture  
      Return $OSArchitecture  
      #Returns 32-bit or 64-bit  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 function Get-CCTK {  
 <#  
      .SYNOPSIS  
           Find CCTK.EXE  
        
      .DESCRIPTION  
           Find the Dell CCTK.EXE file.  
        
      .EXAMPLE  
                     PS C:\> Get-CCTK  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      $Architecture = Get-Architecture  
      If ($Architecture -eq "64-bit") {  
           $Directory = ${env:ProgramFiles(x86)} + "\Dell\"  
           $File = Get-ChildItem -Path $Directory -Filter cctk.exe -Recurse | Where-Object { $_.Directory -like "*_64*" }  
      } else {  
           $Directory = $env:ProgramFiles + "\Dell\"  
           $File = Get-ChildItem -Path $Directory -Filter cctk.exe -Recurse | Where-Object { $_.Directory -like "*x86" }  
      }  
      Return $File  
 }  
   
 function Get-ListOfBIOSSettings {  
 <#  
      .SYNOPSIS  
           Retrieve List of BIOS Settings  
        
      .DESCRIPTION  
           This will get a list of all BIOS settings  
        
      .PARAMETER Executable  
           CCTK.exe  
        
      .EXAMPLE  
           PS C:\> Get-ListOfBIOSSettings  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Executable  
      )  
        
      #Get the path this script is executing from  
      $RelativePath = Get-RelativePath  
      #Get list of exclusions to omit from list of BIOS settings  
      $File = $RelativePath + "BIOSExclusions.txt"  
      $BIOSExclusions = Get-Content -Path $File | Sort-Object  
      #Rewrite list of sorted exclusion back to text file  
      $BIOSExclusions | Out-File -FilePath $File -Force  
      #Get list of BIOS settings -- Script must be executed on a local machine and not from a UNC path  
      $Output = cmd.exe /c $Executable.FullName  
      #Remove instructional information  
      $Output = $Output | Where-Object { $_ -like "*--*" } | Where-Object { $_ -notlike "*cctk*" }  
      #Format Data and sort it  
      $Output = ($Output.split("--") | Where-Object { $_ -notlike "*or*" } | Where-Object{ $_.trim() -ne "" }).Trim() | Where-Object { $_ -notlike "*help*" } | Where-Object { $_ -notlike "*version*" } | Where-Object { $_ -notlike "*infile*" } | Where-Object { $_ -notlike "*logfile*" } | Where-Object { $_ -notlike "*outfile*" } | Where-Object { $_ -notlike "*ovrwrt*" } | Where-Object { $_ -notlike "*setuppwd*" } | Where-Object { $_ -notlike "*sysdefaults*" } | Where-Object { $_ -notlike "*syspwd*" } | ForEach-Object { $_.Split("*")[0] } | Where-Object { $_ -notin $BIOSExclusions }  
      #Add bootorder back in as -- filtered it out since it does not have the -- in front of it  
      $Output = $Output + "bootorder" | Sort-Object  
      Return $Output  
 }  
   
 function Get-BIOSSettings {  
 <#  
      .SYNOPSIS  
           Retrieve BIOS Settings Values  
        
      .DESCRIPTION  
           This will retrieve the value associated with the BIOS Settings  
        
      .PARAMETER Settings  
           List of BIOS Settings  
        
      .PARAMETER Executable  
           CCTK.exe file  
        
      .EXAMPLE  
           PS C:\> Get-BIOSSettings  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Settings,  
           [ValidateNotNullOrEmpty()]$Executable  
      )  
        
      #Create Array  
      $BIOSArray = @()  
      foreach ($Setting in $Settings) {  
           switch ($Setting) {  
                "advbatterychargecfg" {  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "--" + $Setting  
                     $Value = (cmd.exe /c $Arguments).split("=")[1]  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + "--" + $Setting  
                     $Description = (cmd.exe /c $Arguments | Where-Object { $_.trim() -ne "" }).split(":")[1].Trim()  
                }  
                "advsm" {  
                     $Value = ""  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | where-object {$_.trim() -ne ""}).split(":")[1].Trim().split(".")[0]  
                }  
                "bootorder" {  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + $Setting  
                     $Output = (((((cmd.exe /c $Arguments | Where-Object { $_ -like "*Enabled*" } | Where-Object { $_ -notlike "*example*" }) -replace 'Enabled', '').Trim()) -replace '^\d+', '').Trim()) | ForEach-Object { ($_ -split ' {2,}')[1] }  
                     $Output2 = "bootorder="  
                     foreach ($item in $Output) {  
                          [string]$Output2 += [string]$item + ","  
                     }  
                     $Value = $Output2.Substring(0,$Output2.Length-1)  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | where-object { $_.trim() -ne "" }).split(":")[1].Trim().split(".")[0]  
                }  
                "hddinfo" {  
                     $Value = ""  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | where-object {$_.trim() -ne ""}).split(":")[1].trim().split(".")[0]  
                }  
                "hddpwd" {  
                     $Value = ""  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | Where-Object {$_.trim() -ne ""}).split(":")[1].split(".")[0].trim()  
                }  
                "pci" {  
                     $Value = ""  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | Where-Object { $_.trim() -ne "" }).split(":")[1].split(".")[0].trim()  
                }  
                "propowntag" {  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "--" + $Setting  
                     $Value = ((cmd.exe /c $Arguments).split("=")[1]).trim()  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | Where-Object { $_.trim() -ne "" }).split(":")[1].trim()  
                }  
                "secureboot" {  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + " --" + $Setting  
                     $Output = cmd.exe /c $Arguments  
                     if ($Output -like "*not enabled*") {  
                          $Value = "disabled"  
                     } else {  
                          $Value = "enabled"  
                     }  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | where-object { $_.trim() -ne "" }).split(":")[1].Trim().split(".")[0]  
                }  
                default {  
                     #Get BIOS setting  
                     $Output = $null  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "--" + $Setting  
                     $Output = cmd.exe /c $Arguments  
                     #Get BIOS Description  
                     $Arguments = [char]34 + $Executable.FullName + [char]34 + [char]32 + "-h" + [char]32 + "--" + $Setting  
                     $Description = ((cmd.exe /c $Arguments) | Where-Object { $_.trim() -ne "" }).split(":").Trim()[1]  
                     $Value = $Output.split("=")[1]  
                }  
           }  
           #Add Items to object array  
           $objBIOS = New-Object System.Object  
           $objBIOS | Add-Member -MemberType NoteProperty -Name Setting -Value $Setting  
           $objBIOS | Add-Member -MemberType NoteProperty -Name Value -Value $Value  
           $objBIOS | Add-Member -MemberType NoteProperty -Name Description -Value $Description  
           $BIOSArray += $objBIOS  
      }  
      Return $BIOSArray  
 }  
 #Find the CCTK.exe file  
 $CCTK = Get-CCTK  
 #Get List of BIOS settings  
 $BIOSList = Get-ListOfBIOSSettings -Executable $CCTK  
 #Get all BIOS settings  
 $BIOSSettings = Get-BIOSSettings -Executable $CCTK -Settings $BIOSList  
 #Add Computer Model to FileName  
 $FileName = ((Get-WmiObject -Class win32_computersystem -Namespace root\cimv2).Model).Trim()  
 #Add BIOS version and .CSV extension to computer name  
 $FileName += [char]32 + ((Get-WmiObject -Class win32_bios -Namespace root\cimv2).SMBIOSBIOSVersion).Trim() + ".CSV"  
 #Get full path to the output .CSV file  
 If ($FilePath[$FilePath.Length - 1] -ne "\") {  
      $FileName = $FilePath + "\" + $FileName  
 } else {  
      $FileName = $FilePath + $FileName  
 }  
 #Delete old .CSV if it exists  
 If ((Test-Path $FileName) -eq $true) {  
      Remove-Item -Path $FileName -Force  
 }  
 #Screen output  
 $BIOSSettings  
 #File output  
 $BIOSSettings | Export-Csv -Path $FileName -NoTypeInformation -Force  
   

17 July 2017

TPM Readiness Verification

Posted By: Mick Pletcher - 2:04 PM















A while back, I posted a PowerShell script that verified if the TPM was ready for BitLocker to be applied in a build. Recently, the script stopped working. I decided to decipher the code I had borrowed to make the script work. In looking at it, I found a way to significantly simplify the code down to one-liners.

The objective is to verify the TPM is ready for BitLocker encryption before an image is laid down. This is so that if the technician forgets to ready the TPM, it won't go through the entire build process and then fail near the end, thereby wasting a lot of time. There are five steps to verifying this. They are:


  • Verify TPM Ownership is Allowed
  • Verify TPM is Enabled
  • Verify No TPM Ownership
  • Verify TPM is Activated
  • Set the BIOS Password
Each of these steps can be accomplished as a one-liner using PowerShell. As a one-liner, they can be implemented as individual task sequences as shown below.


Each task is setup as a Run Command Line. When WinPE loads, it gathers data in the WMI of the TPM status. I started out using the Get-WMIObject which returned a boolean value. The problem was that MDT does not recognize boolean values. It had to be converted to an integer. The second problem was that executing this via PowerShell would not return the boolean value. It only returned if the expression was successfully executed. That is what the if then else does with the exit 1 or 0. Here are the command lines used along with the required success code.

  • Verify TPM Ownership is Allowed
    • powershell.exe -command "&{if (([int]((Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm).IsOwnerShipAllowed().IsOwnerShipAllowed)) -eq 1) { exit 1 } else { exit 0}}"
    • Success Codes: 1
  • Verify TPM is Enabled
    • powershell.exe -command "&{if (([int]((Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm).IsEnabled().isenabled)) -eq 1) { exit 1 } else { exit 0}}"
    • Success Codes: 1
  • Verify No TPM Ownership
    • powershell.exe -command "&{if (([int]((Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm).isowned().isowned)) -eq 1) { exit 1 } else { exit 0}}"
    • Success Codes: 0
  • Verify TPM is Activated
    • powershell.exe -command "&{if (([int]((Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm).IsActivated().isactivated)) -eq 1) { exit 1 } else { exit 0}}"
    • Success Codes: 1
The other part to this is setting the BIOS password, which also requires CCTK to be installed. For more information on installing the CCTK within the WinPE environment, please refer to this blog entry.

The task sequence to set the BIOS password is shown below. This occurs after the installation of CCTK is done. The task sequence needs to be setup as a Run Command Line. The command line for it is as follows:


  • Set BIOS Password
    • powershell.exe -command "&{If (((Start-Process -FilePath x:\CCTK\cctk.exe -ArgumentList '--setuppwd=<BIOS Password>' -wait -passthru).ExitCode) -eq 115) {(Start-Process -FilePath x:\CCTK\cctk.exe -ArgumentList '--valsetuppwd=<BIOS Password> --setuppwd=<BIOS Password>' -wait -passthru).ExitCode}}"
This command line first tries to set the BIOS password if it is not set. If it is, an error code of 115 is returned and the command line entering the BIOS password is then executed. 

This is all that is required to execute this. Here is a video of the task sequences executing in the build process. 

Here is a video of the task sequences executing at the beginning of the build. 




There is also an alternative to failing at the beginning of the build process. You could have the one-liner create a task sequence variable that would be a flag for a later task just before the BitLocker process starts that would pause the build by initiating the LTISuspend.wsf and pop-up an alert saying to ready the TPM before unpausing the build. We decided to stop the build initially because that reminds the technician that they needed to ready the TPM first. 

MDT: Executing an application multiple times in a task sequence

Posted By: Mick Pletcher - 10:57 AM















Recently, I published a new script that updates all of the Dell drivers on a system automatically. I wanted the script to execute twice in the task sequence with a reboot in between executions. This is so if some drivers or apps do not install the first try due to conflicts with another installation, they will install on the second try.

The first thing I tried was entering the execution as an application and putting the App install in the task sequence twice. During the build, the application would only install once. The second time it skipped over it. To accomplish this, I ended up using a Run Command Line to execute the application more than once and it worked. So if you need to execute an application more than once, use the Run Command Line to do so. 

12 July 2017

Pending Reboot Reporting

Posted By: Mick Pletcher - 10:01 AM














Recently, I implemented Kent Agerlund's technique for monitoring pending reboots located here. This works great, but I also found out there are additional reboot flags on systems that I wanted to monitor. I must say a big thank you to Dean Attali's blog How to Check if a Server Needs a Reboot for providing the information on which registry keys and WMI entries indicate a system is waiting for a reboot. After getting that information, I changed step 5 from Kent's blog with the script below.

The new PowerShell code checks if the system is waiting for a reboot due to windows updates, changes to OS components, pending file rename operations, and if Configuration Manager reboot is pending. All of these are registry queries, except for the Configuration Manager, which is a WMI query.

If you do not want the pending file rename operation, you can comment that line out with a # ($PendingFileRenameOperations = (Get-ItemProperty -Path REGISTRY::"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager" -ErrorAction SilentlyContinue).PendingFileRenameOperations)

To test this, I implemented the new code in the configuration item in SCCM yesterday, already knowing one of the servers needed a reboot. It popped into the collection this morning.


You can download this code from my GitHub site located here.


 <#  
      .SYNOPSIS  
           Reboot Pending Detection  
        
      .DESCRIPTION  
           This script will the four reboot pending flags to verify if a system is pending a reboot. The flags include Windows patches, component based servicing, session manager, and finally configuration manager client.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.141  
           Created on:       7/11/2017 1:10 PM  
           Created by:       Mick Pletcher  
           Filename:         PendingRebootReporting.ps1  
           ===========================================================================  
 #>  
   
 #Checks if the registry key RebootRequired is present. It is created when Windows Updates are applied and require a reboot to take place  
 $PatchReboot = Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue  
 #Checks if the RebootPending key is present. It is created when changes are made to the component store that require a reboot to take place  
 $ComponentBasedReboot = Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction SilentlyContinue  
 #Checks if File rename operations are taking place and require a reboot for the operation to take effect  
 $PendingFileRenameOperations = (Get-ItemProperty -Path REGISTRY::"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager" -ErrorAction SilentlyContinue).PendingFileRenameOperations  
 #Performs a WMI query of the configuration manager service to check if a reboot is pending  
 $ConfigurationManagerReboot = Invoke-WmiMethod -Namespace "ROOT\ccm\ClientSDK" -Class CCM_ClientUtilities -Name DetermineIfRebootPending | select-object -ExpandProperty "RebootPending"  
 If (($PatchReboot -eq $null) -and ($ComponentBasedReboot -eq $null) -and ($PendingFileRenameOperations -eq $null) -and ($ConfigurationManagerReboot -eq $false)) {  
      Return $false  
 } else {  
      Return $true  
 }  
   

07 July 2017

Dell Automatic BIOS, Application, and Driver Updates in Build

Posted By: Mick Pletcher - 12:27 PM















Recently, the Install Dell Command Update and Flash BIOS in WinPE solution I published stopped working when we purchased the Dell E7280. The Dell Command | Update was installing a driver in the WinPE environment that would stick in a continuous installation. Before, it was an all or nothing thing on using the DCU-CLI.exe within WinPE. It could not accept any parameters. I finally figured that problem out. I injected MSI.dll into the %WINDIR%\system32 folder and full functionality of the executable was enabled.

I decided to rework the entire script at that point. The script now operates in both WinPE and the Windows environment. You may ask why you would want to execute this in WinPE. The reason is that if there is a BIOS update needed and you also configure BIOS settings before the OS is laid down, you will want this to be performed first as some BIOS settings cannot be changed after the OS is laid down.

The script detects if it is running in WinPE and will only execute a BIOS update at that point. The way the script works is that it executes the DCU-CLI.exe and uses the /report parameter to only generate a report on what to install. Here is a sample report:




The script then reads the XML report file as shown above to find what to install. In WinPE, it automatically knows to only run the BIOS update. In Windows, it will install all updates if no parameter is defined. You can see in the script that I have defined BIOS, Drivers, and Applications meaning if you select any of these, that is all that will be installed. If you don't select any, all will be installed.

Inside the XML file, there is the <file> field that gives the download address for downloading the driver. After talking with Warren Byle from Dell, I verified that types of updates to include in this script. Warren also verified the release code is unique for every new version of a driver. This gave me the idea to create a local driver repository so they can be downloaded and then executed locally. This saves a lot on time and bandwidth, especially when talking about hundreds of megabytes of data downloads.

The script downloads every driver listed in the XML file to the specified network share outlined in the WindowsRepository parameter. It will first scan the repository folders for one named after the release code and verify the contents matches the file download. If so, it skips downloading.

The next step is installing the drivers. I found that /S works on all Dell driver installations. The only part I had to figure out were the return codes, which are 0, 2, and 3010 for a successful installation.

Another thing I changed was the process of setting this up in MDT. Here are the steps I put in the task sequence processes for running this within WinPE.


I chose to use T: for my mapped drive since I know that driver letter is not used for anything else here. The Map T: Drive task is as shown.


The next step was copying the Dell Client | Update files to WinPE.


These files I copy over were grabbed from the Dell Command | Update directory after it had been installed on a PC.


The next thing I do is to copy the MSI.DLL file to the WinPE system32 directory. I grabbed this file initially from the system32 directory on my Win10 machine.


The next step is unmapping the T: drive. This is necessary because we are going to map that drive letter to the next UNC path.



The next step is to map to the repository location, where the PowerShell script also resides.


We're now going to copy over the PowerShell script. You may wonder why I chose here. That is because this script is intended to be executed both in WinPE and Windows. This directory will be used in Windows, so why not keep the script in the same place instead of having to make a copy of it?


The next step is executing the script. Here is the command line I use: powershell.exe -executionpolicy bypass -file %SystemDrive%\DCU\DellBIOSDriverUpdate.ps1 -WindowsRepository \\<FileShare Repository> -BIOSPassword <Password> -WinPERepository "t:"


The next step is deleting the old XML file. I have the task sequences copy over the XML file to the repository directory in the event I want to look at it. This task sequence deletes the file if it exists.


The next step is to copy the XML to the repository.


Finally, we unmap the T: drive again.


This is all that is to use this script in the WinPE environment.

Next, is using it in the windows environment. This one is much easier. The first thing, don't enter it as an application. If you do, it can only be executed one time in the task sequence. Enter it as a Run Command line task sequence as shown below.



You do need to use the full UNC path and filename for the command line and enter the UNC path under start in.


You maybe wondering what the repository looks like. Here is a pic of the repository that contains the directories labeled after each release version containing the update. The XML files contain the report of all needed drivers derived from the dcu-cli.exe. This is also where I keep this script.



This is a video showing the process of the script in action. I exposed the steps to display on the screen so you could see what it is doing here.


Here is a video of it operating during the build after the OS was laid down.



You can download this script from my GitHub site located here.



 <#  
      .SYNOPSIS  
           Update the BIOS and Drivers  
        
      .DESCRIPTION  
           This script will update the BIOS, Applications, and Drivers. It can detect if it is running within the WinPE or Windows environments. If it is running within WinPE, it will only update the BIOS, otherwise it will run all updates.  
        
      .PARAMETER WindowsRepository  
           UNC path to the updates Windows Repository that is accessible if the operating system is present  
        
      .PARAMETER BIOSPassword  
           Password for the BIOS  
        
      .PARAMETER BIOS  
           Perform BIOS updates only  
        
      .PARAMETER Drivers  
           Perform drivers updates only  
        
      .PARAMETER Applications  
           Perform applications updates only  
        
      .PARAMETER WinPERepository  
           Path to the updates Windows Repository that is accessible if running within WinPE  
        
      .EXAMPLE  
           Running in Windows only and applying all updates  
                powershell.exe -file DellBIOSDriverUpdate.ps1 -WindowsRepository "\\UNCPath2Repository"  
   
           Running in WinPE Only  
                powershell.exe -file DellBIOSDriverUpdate.ps1 -WinPERepository "t:"  
   
           Running in both Windows and WinPE  
                powershell.exe -file DellBIOSDriverUpdate.ps1 -WindowsRepository "\\UNCPath2Repository" -WinPERepository "t:"  
   
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.140  
           Created on:      6/21/2017 3:16 PM  
           Created by:      Mick Pletcher  
           Filename:        DellBIOSDriverUpdate.ps1  
           ===========================================================================  
 #>  
   
 param  
 (  
      [string]$WindowsRepository,  
      [string]$BIOSPassword,  
      [switch]$BIOS,  
      [switch]$Drivers,  
      [switch]$Applications,  
      [string]$WinPERepository  
 )  
   
 function Get-Architecture {  
 <#  
      .SYNOPSIS  
           Get-Architecture  
        
      .DESCRIPTION  
           Returns 32-bit or 64-bit  
        
      .EXAMPLE  
           Get-Architecture  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $OSArchitecture = (Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture).OSArchitecture  
      Return $OSArchitecture  
 }  
   
 function Get-WindowsUpdateReport {  
 <#  
      .SYNOPSIS  
           Get list of updates to install  
        
      .DESCRIPTION  
           Execute the dcu-cli.exe to get a list of updates to install.  
        
      .EXAMPLE  
           PS C:\> Get-WindowsUpdateReport  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([xml])]  
      param ()  
        
      #Test if this is running in the WinPE environment  
      If ((test-path -Path 'REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinPE\') -eq $true) {  
           $Executable = Get-ChildItem -Path "x:\DCU" -Filter dcu-cli.exe  
           $ReportFile = "x:\DCU\DriverReport.xml"  
      } else {  
           $Architecture = Get-Architecture  
           If ($Architecture -eq "32-Bit") {  
                $Executable = Get-ChildItem -Path $env:ProgramFiles"\Dell\CommandUpdate" -Filter dcu-cli.exe  
           } else {  
                $Executable = Get-ChildItem -Path ${env:ProgramFiles(x86)}"\Dell\CommandUpdate" -Filter dcu-cli.exe  
           }  
           #Name and location of the report file  
           If ($WindowsRepository[$WindowsRepository.Length - 1] -ne "\") {  
                $ReportFile = $WindowsRepository + "\" + "DriverReport.xml"  
           } else {  
                $ReportFile = $WindowsRepository + "DriverReport.xml"  
           }  
      }  
      #Delete XML report file if it exists  
      If ((Test-Path -Path $ReportFile) -eq $true) {  
           Remove-Item -Path $ReportFile -Force -ErrorAction SilentlyContinue  
      }  
      #Define location where to write the report  
      $Switches = "/report" + [char]32 + $ReportFile  
      #Get dcu-cli.exe report  
      $ErrCode = (Start-Process -FilePath $Executable.FullName -ArgumentList $Switches -Wait -Passthru).ExitCode  
      #Retrieve list of drivers if XML file exists  
      If ((Test-Path -Path $ReportFile) -eq $true) {  
           #Get the contents of the XML file  
           [xml]$DriverList = Get-Content -Path $ReportFile  
           Return $DriverList  
      } else {  
           Return $null  
      }  
 }  
   
 function Get-WinPEUpdateReport {  
 <#  
      .SYNOPSIS  
           Get Dell Client Update Report  
        
      .DESCRIPTION  
           Execute the Dell Client | Update to generate the XML file listing available updates  
        
      .EXAMPLE  
           PS C:\> Get-WinPEUpdateReport  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Define XML Report File  
      $ReportFile = $env:SystemDrive + "\DCU\DriversReport.xml"  
      #Delete XML Report file  
      If ((Test-Path $ReportFile) -eq $true) {  
           Remove-Item -Path $ReportFile -Force | Out-Null  
      }  
      #Define Dell Client | Update commandline executable  
      $Executable = $env:SystemDrive + "\DCU\dcu-cli.exe"  
      #Define switches for Dell Client | Update  
      $Switches = "/report" + [char]32 + $ReportFile  
      #Execute Dell Client | Update  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -Passthru).ExitCode  
      #Retrieve list of drivers if XML file exists  
      If ((Test-Path -Path $ReportFile) -eq $true) {  
           #Get the contents of the XML file  
           [xml]$DriverList = Get-Content -Path $ReportFile  
           Return $DriverList  
      } else {  
           Return $null  
      }  
 }  
   
 function Update-Repository {  
 <#  
      .SYNOPSIS  
           Update the repository  
        
      .DESCRIPTION  
           This function reads the list of items to be installed and checks the repository to make sure the item is present. If it is not, the item is downloaded to the repository.  
        
      .PARAMETER Updates  
           List of Updates to be installed  
        
      .EXAMPLE  
           PS C:\> Update-Repository  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Updates  
      )  
        
      #Set the variable to the to the repository  
      If ((test-path -Path 'REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinPE\') -eq $true) {  
           If ($WinPERepository[$WinPERepository.Length - 1] -ne "\") {  
                $Repository = $WinPERepository + "\"  
           } else {  
                $Repository = $WinPERepository  
           }  
      } elseif ($WindowsRepository[$WindowsRepository.Length - 1] -ne "\") {  
           $Repository = $WindowsRepository + "\"  
      } else {  
           $Repository = $WindowsRepository  
      }  
      foreach ($Update in $Updates.Updates.Update) {  
           #Define the storage location of this driver  
           $UpdateRepository = $Repository + $Update.Release  
           #Get the URI to download the file from  
           $DownloadURI = $Update.file  
           $DownloadFileName = $UpdateRepository + "\" + ($DownloadURI.split("/")[-1])  
           #Create the new directory if it does not exist  
           If ((Test-Path $UpdateRepository) -eq $false) {  
                New-Item -Path $UpdateRepository -ItemType Directory -Force | Out-Null  
           }  
           #Download file if it does not exist  
           If ((Test-Path $DownloadFileName) -eq $false) {  
                Invoke-WebRequest -Uri $DownloadURI -OutFile $DownloadFileName  
           }  
      }  
 }  
   
 function Update-Applicatons {  
 <#  
      .SYNOPSIS  
           Update Dell Applications  
        
      .DESCRIPTION  
           This function only updates Dell Applications  
        
      .PARAMETER Updates  
           List of updates to install  
        
      .EXAMPLE  
           PS C:\> Update-Applicatons  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Updates  
      )  
        
      if ($WindowsRepository[$WindowsRepository.Length - 1] -ne "\") {  
           $Repository = $WindowsRepository + "\"  
      } else {  
           $Repository = $WindowsRepository  
      }  
      foreach ($Update in $Updates.Updates.Update) {  
           #Check if update is a application update  
           If ($Update.type -eq "Application") {  
                #Get application update file  
                $UpdateFile = $Repository + $Update.Release + "\" + (($Update.file).split("/")[-1])  
                #Verify application update file exists  
                If ((Test-Path $UpdateFile) -eq $true) {  
                     $Output = "Installing " + $Update.name + "....."  
                     Write-Host $Output -NoNewline  
                     # /s to suppress user interface  
                     $Switches = "/s"  
                     $ErrCode = (Start-Process -FilePath $UpdateFile -ArgumentList $Switches -WindowStyle Minimized -Wait -Passthru).ExitCode  
                     If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
                          Write-Host "Success" -ForegroundColor Yellow  
                     } else {  
                          Write-Host "Failed" -ForegroundColor Red  
                     }  
                }  
           }  
      }  
 }  
   
 function Update-BIOS {  
 <#  
      .SYNOPSIS  
           Update the BIOS  
        
      .DESCRIPTION  
           This function will update the BIOS on the system  
        
      .PARAMETER Updates  
           List of updates to install  
        
      .PARAMETER Update  
           XML info of the BIOS update  
        
      .EXAMPLE  
           PS C:\> Update-BIOS  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Updates  
      )  
        
      #Set the variable to the to the repository  
      If ((test-path -Path 'REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinPE\') -eq $true) {  
           If ($WinPERepository[$WinPERepository.Length - 1] -ne "\") {  
                $Repository = $WinPERepository + "\"  
           } else {  
                $Repository = $WinPERepository  
           }  
      } elseif ($WindowsRepository[$WindowsRepository.Length - 1] -ne "\") {  
           $Repository = $WindowsRepository + "\"  
      } else {  
           $Repository = $WindowsRepository  
      }  
      foreach ($Update in $Updates.Updates.Update) {  
           #Check if update is a BIOS update  
           If ($Update.type -eq "BIOS") {  
                #Get BIOS update file  
                $UpdateFile = $Repository + $Update.Release + "\" + (($Update.file).split("/")[-1])  
                #Verify BIOS update file exists  
                If ((Test-Path $UpdateFile) -eq $true) {  
                     $Output = "Installing " + $Update.name + "....."  
                     Write-Host $Output -NoNewline  
                     # /s to suppress user interface  
                     $Switches = "/s /p=" + $BIOSPassword  
                     $ErrCode = (Start-Process -FilePath $UpdateFile -ArgumentList $Switches -WindowStyle Minimized -Wait -Passthru).ExitCode  
                     If (($ErrCode -eq 0) -or ($ErrCode -eq 2) -or ($ErrCode -eq 3010)) {  
                          Write-Host "Success" -ForegroundColor Yellow  
                     } else {  
                          Write-Host "Failed" -ForegroundColor Red  
                     }  
                }  
           }  
      }  
 }  
   
 function Update-Drivers {  
 <#  
      .SYNOPSIS  
           Update Dell Drivers  
        
      .DESCRIPTION  
           This function only updates Dell drivers  
        
      .PARAMETER Updates  
           List of updates to install  
        
      .EXAMPLE  
           PS C:\> Update-Drivers  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$Updates  
      )  
        
      if ($WindowsRepository[$WindowsRepository.Length - 1] -ne "\") {  
           $Repository = $WindowsRepository + "\"  
      } else {  
           $Repository = $WindowsRepository  
      }  
      foreach ($Update in $Updates.Updates.Update) {  
           #Check if update is a application update  
           If ($Update.type -eq "Driver") {  
                #Get driver update file  
                $UpdateFile = $Repository + $Update.Release + "\" + (($Update.file).split("/")[-1])  
                $UpdateFile = Get-ChildItem -Path $UpdateFile  
                #Verify driver update file exists  
                If ((Test-Path $UpdateFile) -eq $true) {  
                     $Output = "Installing " + $Update.name + "....."  
                     Write-Host $Output -NoNewline  
                     # /s to suppress user interface  
                     $Switches = "/s"  
                     $ErrCode = (Start-Process -FilePath $UpdateFile.Fullname -ArgumentList $Switches -WindowStyle Minimized -Passthru).ExitCode  
                     $Start = Get-Date  
                     Do {  
                          $Process = (Get-Process | Where-Object { $_.ProcessName -eq $UpdateFile.BaseName }).ProcessName  
                          $Duration = (Get-Date - $Start).TotalMinutes  
                     } While (($Process -eq $UpdateFile.BaseName) -and ($Duration -lt 10))  
                     If (($ErrCode -eq 0) -or ($ErrCode -eq 2) -or ($ErrCode -eq 3010)) {  
                          Write-Host "Success" -ForegroundColor Yellow  
                     } else {  
                          Write-Host "Failed with error code $ErrCode" -ForegroundColor Red  
                     }  
                }  
           }  
      }  
 }  
   
   
 Clear-Host  
 #Check if running in WinPE environment and get Windows Updates Report  
 If ((test-path -Path 'REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinPE\') -eq $true) {  
      $Updates = Get-WinPEUpdateReport  
 } Else {  
      #Get list of drivers  
      $Updates = Get-WindowsUpdateReport  
 }  
 $Updates.Updates.Update.Name  
 #Process drivers if there is a list  
 If ($Updates -ne $null) {  
      Update-Repository -Updates $Updates  
 }  
 #Check if running in WinPE environment  
 If ((test-path -Path 'REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinPE\') -eq $true) {  
      #Perform BIOS Update  
      Update-BIOS -Updates $Updates  
 } Else {  
      #Install Applications (APP)  
      If (($Applications.IsPresent) -or ((!($Applications.IsPresent)) -and (!($BIOS.IsPresent)) -and (!($Drivers.IsPresent)))) {  
           Update-Applicatons -Updates $Updates  
      }  
      #Install BIOS (BIOS)  
      If (($BIOS.IsPresent) -or ((!($Applications.IsPresent)) -and (!($BIOS.IsPresent)) -and (!($Drivers.IsPresent)))) {  
           Update-BIOS -Updates $Updates  
      }  
      #Install Bundle (SBDL)  
      #Install Drivers (DRVR)  
      If (($Drivers.IsPresent) -or ((!($Applications.IsPresent)) -and (!($BIOS.IsPresent)) -and (!($Drivers.IsPresent)))) {  
           Update-Drivers -Updates $Updates  
      }  
      #Install Firmware (FRMW)  
      #Install ISV Driver (ISVDRVR)  
 }  
   

14 June 2017

MDT Windows Updates Build Report

Posted By: Mick Pletcher - 12:41 PM















I found it nice to be able to get a clean, filtered report on what Windows updates got installed during the build process. This allows me to inject those updates into the MDT Packages so they get injected into the image before it is laid down to speed the process up. I had published this tool two years ago and decided to revamp it to also include email functionality. The tool has given me a report, but there were times I forgot to look at it after a build completed. This reminds me by sending the report out via email.

The way this tool works is by reading the ZTIWindowsUpdate.log file from the c:\minint\smsosd\osdlogs directory and extracting the list of installed Windows Updates. The script filters out everything that is non-windows updates, such as Dell drivers. It also filters out the windows defender updates since those are cumulative and gets updated on a regular basis. 

This is a screenshot of what the logs look like when executed and output to the screen:



Here is a screenshot of what the same report looks like when opened up in Excel. 


The script extracts the KB article number and description and writes that information to an object. The object is then displayed on the screen and written to a .CSV file. It is sorted by KBArticle number. 

The firm I work at uses Dell machines and in doing so I excluded all Dell drivers from the list. There is also an exclusions.txt file it can read from to input items you may want to exclude from the list. I added "*Advanced Micro Devices*" as one item in my TXT file. The exclusions.txt file should reside in the same directory as the script. 

The script has been tested when a system is connected to the domain (Final Image) and when it belongs to a workgroup (Reference Image). It works in both instances.

I have pre-populated all parameters, except From, To, and SMTPServer. Those were left blank since you would likely want to populate them at the command line. 

Here is an example:

powershell.exe -file WindowsUpdatesReport.ps1 -email -From IT@Testcompany.com -To mickpletcher@testcompany.com -SMTPServer smtp.testcompany.com

I have pre-populated the -OutputFile, -ExclusionsFile, -Subject, and -Body. You can go into the script and change those or decide to override them by defining them at the command line. You could also populate the -From, -To, and -SMTPServer if you like. 

Here is a screenshot of how it is setup in the MDT task sequence the first time. This did not work. 


And this is a filtered screenshot of how it is setup under as an application install:



I tried one more way to execute it and it finally worked as shown below:


The command line I used is: powershell.exe -executionpolicy bypass -file <UNC path>\WindowsUpdatesReport.ps1 -Email -From <sender's email address> -To <recipient's email address> -SMTPServer <SMTP server address>

The start in contains the <UNC path> where the script resides.

You can download the file from my GitHub location


One more thing I wanted to mention is SAPIEN's PowerShell Studio. This made writing this script an absolute breeze. I highly recommend using it. 


 <#  
      .SYNOPSIS  
           Generate Windows Updates Report  
        
      .DESCRIPTION  
           This script will extract the list of windows updates installed  
           during an MDT build.  
        
      .PARAMETER OutputFile  
           File to write the list of installed updates to.  
        
      .PARAMETER ExclusionsFile  
           Text file containing a list of update descriptions to exclude from the report  
        
      .PARAMETER Email  
           Send an email to the specified IT staff with the attached .csv file containing a list of all updates installed during the build process.  
        
      .PARAMETER From  
           Email Sender  
        
      .PARAMETER To  
           Email Recipient  
        
      .PARAMETER SMTPServer  
           SMTPServer  
        
      .PARAMETER Subject  
           Email Subject  
        
      .PARAMETER Body  
           Body contents  
        
      .EXAMPLE  
           powershell.exe -executionpolicy bypass -file WindowsUpdatesReport.ps1 -OutputFile BaseBuild.csv -Path \\NetworkLocation\Directory  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:      5/31/2017 10:12 AM  
           Created by:      Mick Pletcher  
           Filename:        WindowsUpdatesReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$OutputFile = 'WindowsUpdatesReport.csv',  
      [ValidateNotNullOrEmpty()][string]$ExclusionsFile = 'Exclusions.txt',  
      [switch]$Email,  
      [string]$From,  
      [string]$To,  
      [string]$SMTPServer,  
      [string]$Subject = 'Windows Updates Build Report',  
      [string]$Body = "List of windows updates installed during the build process"  
 )  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 function Remove-OutputFile {  
 <#  
      .SYNOPSIS  
           Delete Output File  
        
      .DESCRIPTION  
           This function deletes the old output file that contains a list of updates that were installed during a build.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Get the path this script is executing from  
      $RelativePath = Get-RelativePath  
      #Define location of the output file  
      $File = $RelativePath + $OutputFile  
      If ((Test-Path -Path $File) -eq $true) {  
           Remove-Item -Path $File -Force  
      }  
 }  
   
 function Get-Updates {  
 <#  
      .SYNOPSIS  
           Retrieve the list of installed updates  
        
      .DESCRIPTION  
           This function retrieves the list of updates that were installed during the build process  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([array])]  
      param ()  
        
      $UpdateArray = @()  
      #Get the path this script is executing from  
      $RelativePath = Get-RelativePath  
      #File containing a list of exclusions  
      $ExclusionsFile = $RelativePath + $ExclusionsFile  
      #Get list of exclusions from exclusions file  
      $Exclusions = Get-Content -Path $ExclusionsFile  
      #Locate the ZTIWindowsUpdate.log file  
      $FileName = Get-ChildItem -Path $env:HOMEDRIVE"\minint" -filter ztiwindowsupdate.log -recurse  
      #Get list of all installed updates except for Windows Malicious Software Removal Tool, Definition Update for Windows Defender, and Definition Update for Microsoft Endpoint Protection  
      $FileContent = Get-Content -Path $FileName.FullName | Where-Object { ($_ -like "*INSTALL*") } | Where-Object { $_ -notlike "*Windows Defender*" } | Where-Object { $_ -notlike "*Endpoint Protection*" } | Where-Object { $_ -notlike "*Windows Malicious Software Removal Tool*" } | Where-Object { $_ -notlike "*Dell*" } | Where-Object { $_ -notlike $Exclusions }  
      #Filter out all unnecessary lines  
      $Updates = (($FileContent -replace (" - ", "~")).split("~") | where-object { ($_ -notlike "*LOG*INSTALL*") -and ($_ -notlike "*ZTIWindowsUpdate*") -and ($_ -notlike "*-*-*-*-*") })  
      foreach ($Update in $Updates) {  
           #Create object  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           #Add KB article number to object  
           $Object | Add-Member -MemberType NoteProperty -Name KBArticle -Value ($Update.split("(")[1]).split(")")[0].Trim()  
           #Add description of KB article to object  
           $Description = $Update.split("(")[0]  
           $Description = $Description -replace (",", " ")  
           $Object | Add-Member -MemberType NoteProperty -Name Description -Value $Description  
           #Add the object to the array  
           $UpdateArray += $Object  
      }  
      If ($UpdateArray -ne $null) {  
           $UpdateArray = $UpdateArray | Sort-Object -Property KBArticle  
           #Define file to write the report to  
           $OutputFile = $RelativePath + $OutputFile  
           $UpdateArray | Export-Csv -Path $OutputFile -NoTypeInformation -NoClobber  
      }  
      Return $UpdateArray  
 }  
   
 Clear-Host  
 #Delete the old report file  
 Remove-OutputFile  
 #Get list of installed updates  
 Get-Updates  
 If ($Email.IsPresent) {  
      $RelativePath = Get-RelativePath  
      $Attachment = $RelativePath + $OutputFile  
      #Email Updates  
      Send-MailMessage -From $From -To $To -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Attachments $Attachment  
 }  
   

30 May 2017

Find Maximum Possible Resolution for Each Monitor

Posted By: Mick Pletcher - 3:47 PM















I have been working on a way that I can ensure the maximum resolution is set on monitors. Every so often, a monitor does not have the resolution set to maximum. I have been trying to figure out a way to set the resolution to maximum for more than a year, especially when a system is built in the build room and then moved to the user's office to be setup with new monitors. The problem I have had was being able to get the maximum resolution value. It is not stored in the system that is easily accessible through PowerShell. I had thought about having the script go through the keystroke process of the Display screen to set the monitor resolution to maximum and then write the resolution values to a text file. PowerShell can retrieve the resolution monitors are currently set to. Finally, it occurred to me the maximum resolution should be stored in the INF driver file. I opened up the file and there it was.

Andy Schneider has this awesome script that can set the resolution of the monitors. The only part was needed were the resolution values. The script below can be used with Andy's to set the resolution to maximum for each installed monitor. The Get-MaximumResolution function returns an array of objects containing the model, horizontal, and vertical resolutions.

Here is the output of the script after it was executed on my machine that has three monitors.



I also want to point out that SAPIEN's PowerShell Studio made writing the script a breeze. It simplifies the process of this and allows for much more thorough scripting.

The script is available to download from my GitHub site located here.


 <#  
      .SYNOPSIS  
           Get Maximum Monitor Resolution  
        
      .DESCRIPTION  
           This script will retrieve the maximum possible resolution for monitors by identifying the associated driver. The driver INF file contains the maximum defined resolution for a monitor. This script is designed for Dell monitors only. It has not been tested on any other brand. Also, the monitors need to be installed in the device manager to get the correct association.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/30/2017 12:37 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         MaxResolution.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Get-MaximumResolution {  
      #Create monitor array  
      $Monitors = @()  
      #Get associate monitor hardware ID for each monitor  
      $HardwareIDs = (Get-WmiObject Win32_PNPEntity | where-object { $_.PNPClass -eq "Monitor" }).HardwareID | ForEach-Object { $_.Split("\")[1] }  
      foreach ($Monitor in $HardwareIDs) {  
           #Create object  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           #Get the location of the associated driver file  
           $DriverFile = Get-ChildItem -path c:\windows\system32\driverstore -Filter *.inf -recurse | Where-Object { (Select-String -InputObject $_ -Pattern $Monitor -quiet) -eq $true }  
           #Retrieve the maximum resolution from the INF file  
           $MaxResolution = ((Get-Content -Path $DriverFile.FullName | Where-Object { $_ -like "*,,MaxResolution,,*" }).split('"')[1]).Split(",")  
           #Write the Model to the object  
           $Object | Add-Member -MemberType NoteProperty -Name Model -Value $DriverFile.BaseName.ToUpper()  
           #Write the horizontal maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Horizontal(X)" -Value $MaxResolution[0]  
           #Write the vertical maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Vertical(Y)" -Value $MaxResolution[1]  
           #Write the object to the array  
           $Monitors += $Object  
      }  
      Return $Monitors  
 }  
   
 #Display list of monitor with maximum available resolutions  
 $Monitors = Get-MaximumResolution  
 $Monitors  
   

23 May 2017

Pending Reboot Reporting with Orchestrator

Posted By: Mick Pletcher - 1:45 PM















As we are implementing the ADR in SCCM for servers, we want to know if systems are pending a reboot without having to log into every server. Thankfully, Kent Agerlund, formulated and posted this awesome solution for tracking pending reboots using a compliance rule and baseline in SCCM. It reports systems that are waiting for a reboot to a non-compliance collection.

I wanted to take this to the next level with automated reporting via email. I wrote the script below that queries the non-compliance collection in SCCM and writes the results to a .CSV file and emails that file to the appropriate IT staff. I integrated this with Orchestrator so this process becomes an automated process.

To implement this in Orchestrator, you will need to use the monitor date/time activity to schedule the execution. I have it scheduled for every day.



Next, you link a Run Program activity to run the PowerShell script.



The next activity is the Get File Status. This will check to see if the .CSV file exists. The PowerShell script will delete the old .CSV file and will not create a new one if there are no systems pending a reboot.



The next thing is to customize the link between the Get File Status and Send Email activities. This stops the Send Email from taking place if the .CSV file is not present.


Finally, the Send Email activity is executed to send an email to the appropriate IT staff with the attached .CSV file.


Here is a screenshot of my runbook.

I have included in the script an example of how to implement this in the command line. You can download the PowerShell script from my GitHub site located here.

I also wanted to say how much easier SAPIEN's PowerShell Studio made writing this script. PowerShell studio is a fantastic PowerShell editing tool that takes coding to a whole new level.

 <#  
      .SYNOPSIS  
           Reboot Report  
        
      .DESCRIPTION  
           This script will query SCCM for a list of machines pending a reboot. It will then write the list to a .CSV file.  
        
      .PARAMETER CollectionName  
           Name of the collection to query for a list of machines  
        
      .PARAMETER SCCMServer  
           Name of the SCCM Server  
        
      .PARAMETER SCCMDrive  
           Drive of the SCCM server  
        
      .PARAMETER ReportFile  
           Name of the file to write the list of systems pending a reboot.  
        
      .EXAMPLE  
           powershell.exe -file RebootReporting.ps1 -CollectionName "All Servers" -SCCMServer ACMESCCM -SCCMDrive CMG -ReportFile "PendingRebootReport.csv"  
   
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/22/2017 2:42 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         RebootReporting.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$CollectionName,  
      [ValidateNotNullOrEmpty()][string]$SCCMServer,  
      [ValidateNotNullOrEmpty()][string]$SCCMDrive,  
      [ValidateNotNullOrEmpty()][string]$ReportFile  
 )  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Get-RebootPendingSystems {  
 <#  
      .SYNOPSIS  
           Reboot Pending Systems  
        
      .DESCRIPTION  
           This function connects to SCCM and retrieves the list of systems pending a reboot.  
        
      .EXAMPLE  
                     PS C:\> Get-RebootPendingSystems  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Create Report array  
      $Report = @()  
      #If the SCCM drive does not have a colon at the end, add it  
      If ($SCCMDrive[$SCCMDrive.Length - 1] -ne ":") {  
           $SCCMDrive = $SCCMDrive + ":"  
      }  
      #Change the location to the SCCM drive  
      Set-Location $SCCMDrive  
      #Get list of systems in the SCCM collection that are pending a reboot  
      $Systems = (Get-CMDevice -collectionname $CollectionName).Name | Sort-object  
      foreach ($System in $Systems) {  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $System.ToUpper()  
           $Report += $Object  
      }  
      #Change location back to the system homedrive  
      Set-Location $env:HOMEDRIVE  
      #Return the list of systems  
      Return $Report  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 Clear-Host  
 #Find and import the PowerShell SCCM Module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Get a list of systems pending a reboot  
 $Report = Get-RebootPendingSystems  
 #Get the path this script is being executed from  
 $RelativePath = Get-RelativePath  
 #Add the relative path to the filename  
 $ReportFile = $RelativePath + $ReportFile  
 #Delete Report File if it exists  
 If ((Test-Path $ReportFile) -eq $true) {  
      Remove-Item -Path $ReportFile -Force  
 }  
 If (($Report -ne $null) -and ($Report -ne "")) {  
      #Display the list of systems to the screen  
      $Report  
      #Export the list of systems to a CSV file  
      $Report | Export-Csv -Path $ReportFile -Encoding UTF8 -Force -NoTypeInformation  
 }  
   

19 May 2017

Local Administrators Automated Reporting Tool

Posted By: Mick Pletcher - 3:04 PM















Back in November 2016, I posted the blog entry on reporting local administrators on machines. That script is deployed to machines via an SCCM package that reports the local administrators back to SCCM to be able to be queried into a report.

I got tired of having to go into SCCM and look at the query report to see if any new machines might have appeared. I decided to write this script that would automate the process. This script is designed to be used with System Center Orchestrator or it can be setup as a scheduled task. It is written to perform a query in SCCM to display the data the above-listed script reported to SCCM to the screen and to a CSV file.

I have set mine up in Orchestrator. Here is the process I used in setting mine up:

The first is setup to run this every day. The second points to the Daily Schedule properties defining which days to run it on. The third is the run program defining to run the PowerShell script. Here is how it is laid out:


The fourth one is defining to send an email with the CSV attachment. Here is how it is setup:


I wanted to take a moment and say how much SAPIEN PowerShell Studio made a difference with writing this code. It made the process so much easier and more efficient.

You can download the script from GitHub.


 <#  
      .SYNOPSIS  
           Execute SCCM Stored Queries  
        
      .DESCRIPTION  
           This script will execute SCCM stored queries.  
        
      .PARAMETER ListQueries  
           Generate a list of queries  
        
      .PARAMETER Query  
           Name of the query to execute  
        
      .PARAMETER SCCMServer  
           Name of SCCM server  
        
      .PARAMETER SCCMServerDrive  
           A description of the SCCMServerDrive parameter.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/8/2017 1:32 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMQuery.ps1  
           ===========================================================================  
 #>  
 param  
 (  
      [switch]$ListQueries,  
      [string]$Query,  
      [string]$SCCMServer,  
      [string]$SCCMServerDrive  
 )  
   
 function Get-ListOfQueries {  
 <#  
      .SYNOPSIS  
           Get List of Queries  
        
      .DESCRIPTION  
           This function will retrieve a list of all queries in SCCM  
        
      .EXAMPLE  
                     PS C:\> Get-ListOfQueries  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      Set-Location $SCCMServerDrive  
      $Queries = Get-CMQuery  
      Set-Location $env:SystemDrive  
      $QueryArray = @()  
      foreach ($Query in $Queries) {  
           $QueryArray += $Query.Name  
      }  
      $QueryArray = $QueryArray | Sort-Object  
      $QueryArray  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 function Get-SCCMQueryData {  
      [CmdletBinding()]  
      param ()  
        
      $Report = @()  
      #Change directory to the SCCM server drive  
      Set-Location $SCCMServerDrive  
      #Retrieve report from SCCM  
      $Output = Get-CMQuery -Name $Query | Invoke-CMQuery  
      #Change directory back to the system this script is running on  
      Set-Location $env:SystemDrive  
      #Parse through data and create report object  
      foreach ($Item in $Output) {  
           $Item1 = [string]$Item  
           $Domain = (($Item1.split(';'))[0]).Split('"')[1]  
           $User = ((($Item1.split(";"))[1]).Split('"'))[1]  
           $ComputerName = ((($Item1.split(";"))[3]).Split('"'))[1]  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $ComputerName.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name Domain -Value $Domain.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name UserName -Value $User.ToUpper()  
           $Report += $Object  
      }  
      $Report = $Report | Sort-Object -Property UserName  
      Return $Report  
 }  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Send-Report {  
 <#  
      .SYNOPSIS  
           Email report  
        
      .DESCRIPTION  
           A detailed description of the Send-Report function.  
        
      .EXAMPLE  
                     PS C:\> Send-Report  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #TODO: Place script here  
 }  
   
 Clear-Host  
 #Add colon to end of SCCMServerDrive if not present  
 If ($SCCMServerDrive[$SCCMServerDrive.Length - 1] -ne ":") {  
      $SCCMServerDrive += ":"  
 }  
 #Import SCCM module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Generate a list of all available queries in SCCM  
 If ($ListQueries.IsPresent) {  
      Get-ListOfQueries  
 }  
 #If query is not filled in, then end the script  
 If (($Query -ne $null) -and ($Query -ne "")) {  
      #Perform query from SCCM  
      $Report = Get-SCCMQueryData | Sort-Object -Property ComputerName  
      #Display report to screen  
      $Report  
      #Get path where this script is executing from  
      $RelativePath = Get-RelativePath  
      #Location where to write the report to  
      $File = $RelativePath + "LocalAdministrators.csv"  
      #Delete old report if it exists  
      If ((Test-Path $File) -eq $true) {  
           Remove-Item -Path $File -Force  
      }  
      #Write new report to CSV file  
      $Report | Export-Csv -Path $File -Encoding UTF8 -Force  
 }  
   

Copyright © 2013 Mick's IT Blogs™ is a registered trademark.