08 August 2019

Verifying SCCM package has updated the content on the distribution point

One of the things I have wrestled with over the years of using SCCM is verifying if the package on the distribution point contains the new content after updating the distribution point(s). I have typically seen that if there is a minor change, such as maybe a line of two of code changed in a PowerShell installer script, it doesn't get updated the first time around.

While working on the migration of the imaging process from MDT to SCCM, it occurred to me that there is a way to verify. I have typically run the update distribution points at least twice to make sure they all got updated. The easy way to verify is to set up a package share in the properties as shown below.


Now you can watch the package located at \\<SCCM Server Name>\<Share name> to see if the changes take place. They will change in real-time as soon as SCCM updates it. If you are in a testing phase and are having to update the distribution point(s) a lot like I am right now, this is a good way to test after each update.

NOTE: There still is not a way to do this with application deployments. It would be nice if the share option was added there! 

02 August 2019

MDT: How to initiate a reboot during a task without corrupting the task sequence

Recently, I have been working on updating several scripts I have written for the build process. One big thing I have wanted is for the script to be able to initiate a reboot without the build process becoming corrupt. An additional functionality I have wanted to implement is to be able to restart the task sequence at the same point it left off before the reboot.

I knew the task sequence reruns the windows update process multiple times, so I started by looking at the ZTIWindowsUpdate.wsf file. While combing through the file, I found the two items used for this process. They are:

  • SMSTSRebootRequested that reboots the machine
  • SMSTSRetryRequested tells the script to rerun the same task. 
I wanted this to be written in PowerShell, so next was figuring out how to access these two MDT environmental variables. After researching, I found that I can load the comobject Microsoft.SMS.TSEnvironment and that will give access to them. The following three lines of code are all that is needed in a PowerShell script to initiate a reboot and/or rerun the same task once it completes. Setting the above-listed variables to $true is what is required. If you just want to rerun the same task, you are not required to initiate the reboot. Once the task is completed, it will rerun again if SMSTSRetryRequested is specified at the end. 


      $TaskSequence = New-Object -ComObject Microsoft.SMS.TSEnvironment  
      #Reboot the machine this command line task sequence finishes  
      $TaskSequence.Value('SMSTSRebootRequested') = $true  
      #Rerun this task when the reboot finishes  
      $TaskSequence.Value('SMSTSRetryRequested') = $true  

18 July 2019

MDT Conditional Reboot

I wrote an article about three years ago on conditional task sequence reboots. It used the built-in reboot task sequence that was initiated only if any of the three conditions were met. The problem was a fourth condition that could not be tested for because a WMI query is the only way to test and MDT conditions do not incorporate WMI.

Recently, I revisited this, and it occurred to me how to incorporate the WMI query after going through the ZTIWindowsUpdate.wsf and seeing how it initiated reboots. I abandoned the built-in reboot and wrote a PowerShell script that can test all four conditions and then connect to the TSEnvironment object to start a reboot.

NOTE: The fourth test depends on the SCCM client already being installed.

The four conditions the script checks for are:

  • Component Based Servicing
  • Windows Updates
  • Pending Files Rename
  • Pending reboot from SCCM installs
The script will iterate through all four conditions. If a condition is met, it will then connect to the TSEnvironment object and request a reboot by setting SMSTSRebootRequested to true. Once the script is finished, the system will reboot and then proceed to the next task.

I also included the commented out SMSTSRetryRequested in the script. This command will cause the task sequence to rerun this script. I included it in here so if you want to take the code from this script and incorporate it into another script that will rerun it after the reboot, it is there. 

The first thing to do is to copy the script to the scripts (%SCRIPTROOT%) directory. As you can see in the screenshot below, I used a Run Command Line task sequence.



The command line is as follows:

 powershell.exe -executionpolicy bypass -file "%SCRIPTROOT%\ZTIConditionalReboot.ps1"  


Finally, here is the script. You can download it from my GitHub site.

 <#  
      .SYNOPSIS  
           Zero Touch Conditional Reboot  
        
      .DESCRIPTION  
           This script will check four flags on the system to see if a reboot is required. If one of the flags is tripped, then this script will initiate a reboot in MDT so that will come back up and start at the proceeding task. I have included the commented out SMSTSRetryRequested in the script so if you want to incorporate the code from this script into another one that will need to be rerun again once the reboot completes.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       7/12/2019 2:53 PM  
           Created by:       Mick Pletcher  
           Organization:     Waller Lansden Dortch & Davis, LLP.  
           Filename:         ZTIConditionalReboot.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Enable-Reboot {  
 <#  
      .SYNOPSIS  
           Request MDT Reboot  
        
      .DESCRIPTION  
           A detailed description of the Enable-Reboot function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      $TaskSequence = New-Object -ComObject Microsoft.SMS.TSEnvironment  
      #Reboot the machine this command line task sequence finishes  
      $TaskSequence.Value('SMSTSRebootRequested') = $true  
      #Rerun this task when the reboot finishes  
      #$TaskSequence.Value('SMSTSRetryRequested') = $true  
 }  
   
 #Component Based Reboot  
 If ((Get-ChildItem "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Windows Update Reboot  
 } elseif ((Get-Item -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Pending Files Rename Reboot  
 } elseif ((Get-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Pending SCCM Reboot  
 } elseif ((([wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities").DetermineIfRebootPending().RebootPending) -eq $true) {  
      Enable-Reboot  
 } else {  
      Exit 0  
 }  
   


11 June 2019

Fixing Do you want to run this file? during SCCM Deployment

Over the past two months, I deployed the Windows 10 1809 to all Windows 10 machines. We got through 80% of the machines with successful deployments until we reached those last 20% where they did not have enough disk space for both downloading the package and installing it. The package and install require roughly 15 gigs of space, 5 gigs for the package and 10 gigs for the installation.

We changed the distribution points option to run the program from the distribution point to drop down the 15 gig requirement to 10 gigs, which also significantly reduced the number of machines without enough space.


Once we made that change, systems started getting the message shown below. This caused the package to stall since there was no one to click run.



To get around this issue, we added the FQDN of each distribution point to the intranet trusted sites via GPO. The FQDN is added like shown here: \\<SCCMServer.contoso.com>\ and set as security zone 1. This fixed that issue. 

06 June 2019

Configuring Power Scheme with a PowerShell One-Liner

Recently, we decided to change the power scheme on machines during the build. This can be quickly done using the powercfg.exe, but I wanted to be sure it always set correctly. Plus, the GUID associated with a power scheme can be different, so I wanted to specify the power scheme by the name.

This PowerShell one-liner will set the power scheme on a machine to the scheme defined in the variable $Setting. If you do a powercfg.exe /l, you will see the name displayed to the right of the GUID in parenthesis. That is what you define in the above variable. The one-liner will then query powercfg to check if it matches the $Setting variable. If it does, it exits with an error code 0. If it does not match, then it sets the power scheme to the GUID from the query and then rechecks to make sure the setting was configured. If it still does not match, then it exits with an error code 1. 

To use this in a one-liner, you need to define the $Setting inside the one-liner below. This may differ on machines, so do the above query to see what is defined in your environment. Place this one-liner in a command line task sequence and you are done.


 powershell.exe -executionpolicy bypass -command "&{$Setting='Balanced';$Output = powercfg.exe /l | Where-Object {$_ -like ('*' + $Setting + '*')};If ($Output.Contains('*') -eq $true) {Write-Host ($Setting + [char]32 + 'is configured');Exit 0} else {$Output = powercfg.exe /s $output.split(' ')[3]; $Output = powercfg.exe /l | Where-Object {$_ -like ('*' + $Setting + '*')};If ($Output.Contains('*') -eq $true) {Write-Host ($Setting + [char]32 + 'Powercfg is configured');Exit 0} else {Write-Host 'Powercfg failed';Exit 1}}}"  

Below is a pic of what it looks like when used in a Run Command Line task sequence. IMO, it makes managing PowerShell scripts easier when they are contained within the task sequence.


15 April 2019

Configuring Wake-On-LAN for Dell Systems

If you have been wanting to wake your Dell systems up from sleep, hibernate, or shutdown states, this is how you do it. Starting out with this article from Dell, I got the list of things needed to set up the system for WOL. There are three areas that have to be configured on Dell systems, at least for the systems we have which range from the Optiplex 990 to the Latitude 7490. The areas are BIOS, advanced NIC, and power management settings. This site helped with the final setting to disable fast startup, which is required. WOL did not work on our systems until I implemented this final setting.

Before implementing this baseline, you will need to make sure Dell Command | Configure is installed on all systems. To ensure this, I have it deployed as an application to all Dell systems. Dell Command | Configure is what the baseline PowerShell scripts use to query and configure the BIOS settings. I also made a collection called All Dell Systems since we also have a few Microsoft Surfaces.

NOTE: This was created on April 15, 2019. New Dell models and BIOS updates are constantly released. It is likely there will be changes that need to be made in the future to these scripts to work with those updates.

The Wake-On-LAN Compliance item is set up to use a script that returns a Boolean value as shown below.


The discovery script is the following PowerShell script:


 ##Find Dell Command | Configure for 64-bit  
 $CCTK = Get-ChildItem -Path ${env:ProgramFiles(x86)}, $env:ProgramFiles -Filter cctk.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.Directory -like '*x86_64*'}  
 ##Get all available Dell Command | Configure commands for current system  
 $Commands = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName -h} -ErrorAction SilentlyContinue  
 ##Configure BIOS --wakeonlan=enable  
 #Test if wakeonlan exists on current system  
 If ($Commands -like '*wakeonlan*') {  
      [string]$WakeOnLANSetting = 'wakeonlan=enable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --wakeonlan} -ErrorAction SilentlyContinue  
      If ($Output -ne $WakeOnLANSetting) {  
           $WakeOnLAN = $false  
      } else {  
           $WakeOnLAN = $true  
      }  
 }  
 ##Configure BIOS --deepsleepctrl=disable  
 #Test if deepsleepctrl exists on current system  
 If ($Commands -like '*deepsleepctrl*') {  
      [string]$DeepSleepCtrlSetting = 'deepsleepctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --deepsleepctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $DeepSleepCtrlSetting) {  
           $DeepSleepCtrl = $false  
      } else {  
           $DeepSleepCtrl = $true  
      }  
 }  
 ##Configure BIOS --blocks3=disable  
 #Test if blocks3 exists on current system  
 If ($Commands -like '*blocks3*') {  
      [string]$BlockS3Setting = 'blocks3=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --blocks3} -ErrorAction SilentlyContinue  
      If ($Output -ne $BlockS3Setting) {  
           $BlockS3 = $false  
      } else {  
           $BlockS3 = $true  
      }  
 }  
 ##Configure BIOS --cstatesctrl=disable  
 #Test if cstatesctrl exists on current system  
 If ($Commands -like '*cstatesctrl*') {  
      [string]$CStateCTRLSetting = 'cstatesctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --cstatesctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $CStateCTRLSetting) {  
           $CStateCTRL = $false  
      } else {  
           $CStateCTRL = $true  
      }  
 }  
 ##Disable Energy Efficient Ethernet  
 #Energy Efficient Ethernet disable registry value  
 $RegistryValue = '0'  
 #Find ethernet adapter  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Efficient Ethernet*'}).DisplayName  
 #Test for presence of Energy-Efficient Ethernet  
 If ($DisplayName -like '*Efficient Ethernet*') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           $EnergyEfficientEthernet = $false  
      } else {  
           $EnergyEfficientEthernet = $true  
      }  
 }  
 ##Enable Wake on Magic Packet  
 $State = 'Enabled'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Magic Packet*'}).DisplayName  
 #Test if Magic Packet exists  
 If ($DisplayName -like '*Magic Packet*') {  
      [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
      If ($CurrentState -ne $State) {  
           $WakeOnMagicPacket = $false  
      } else {  
           $WakeOnMagicPacket = $true  
      }  
 }  
 ##Disable Shutdown Wake-On-Lan  
 $RegistryValue = '0'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -eq 'Shutdown Wake-On-Lan'}).DisplayName  
 If ($DisplayName -eq 'Shutdown Wake-On-Lan') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           $ShutdownWakeOnLAN = $false  
      } else {  
           $ShutdownWakeOnLAN = $true  
      }  
 }  
 ##Enable Allow the computer to turn off this device  
 $KeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\'  
 #Test if KeyPath exists  
 If ((Test-Path $KeyPath) -eq $true) {  
      $PnPValue = 256  
      $Adapter = Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}  
      foreach ($Entry in (Get-ChildItem $KeyPath -ErrorAction SilentlyContinue).Name) {  
           If ((Get-ItemProperty REGISTRY::$Entry).DriverDesc -eq $Adapter.InterfaceDescription) {  
                $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                If ($Value -ne $PnPValue) {  
                     $PowerManagement = $false  
                } else {  
                     $PowerManagement = $true  
                }  
           }  
      }  
 }  
 ##Disable Fast Startup  
 $KeyPath = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Power"  
 #Test if KeyPath exists  
 If ((Test-Path -Path ('REGISTRY::' + $KeyPath)) -eq $true) {  
      If ((Get-ItemProperty -Path ('REGISTRY::' + $KeyPath)).HiberbootEnabled -eq 0) {  
           $FastStartup = $false  
      } else {  
           $FastStartup = $true  
      }  
 }  
 #Write-Host 'Wake-On-LAN:'$WakeOnLAN  
 #Write-Host 'Deep Sleep Control:'$DeepSleepCtrl  
 #Write-Host 'BlockS3:'$BlockS3  
 #Write-Host 'CState Control:'$CStateCTRL  
 #Write-Host 'Energy Efficient Ethernet:'$EnergyEfficientEthernet  
 #Write-Host 'Wake-On-Magic-Packet:'$WakeOnMagicPacket  
 #Write-Host 'Shutdown Wake-On-LAN:'$ShutdownWakeOnLAN  
 #Write-Host 'Allow Computer to Turn Off this Device:'$PowerManagement  
 If ((($WakeOnLAN -eq $null) -or ($WakeOnLAN -eq $true)) -and ($FastStartup -eq $false) -and (($DeepSleepCtrl -eq $null) -or ($DeepSleepCtrl -eq $true)) -and (($BlockS3 -eq $null) -or ($BlockS3 -eq $true)) -and (($CStateCTRL -eq $null) -or ($CStateCTRL -eq $true)) -and (($EnergyEfficientEthernet -eq $null) -or ($EnergyEfficientEthernet -eq $true)) -and (($WakeOnMagicPacket -eq $null) -or ($WakeOnMagicPacket -eq $true)) -and (($ShutdownWakeOnLAN -eq $null) -or ($ShutdownWakeOnLAN -eq $true)) -and (($PowerManagement -eq $null) -or ($PowerManagement -eq $true))) {  
      echo $true  
 } else {  
      echo $false  
 }  
   

The remediation script is as follows:


 ##Find Dell Command | Configure for 64-bit  
 $CCTK = Get-ChildItem -Path ${env:ProgramFiles(x86)}, $env:ProgramFiles -Filter cctk.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.Directory -like '*x86_64*'}  
 ##Get all available Dell Command | Configure commands for current system  
 $Commands = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName -h} -ErrorAction SilentlyContinue  
 ##Configure BIOS --wakeonlan=enable  
 #Test if wakeonlan exists on current system  
 If ($Commands -like '*wakeonlan*') {  
      [string]$WakeOnLANSetting = 'wakeonlan=enable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --wakeonlan} -ErrorAction SilentlyContinue  
      If ($Output -ne $WakeOnLANSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $WakeOnLANSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $WakeOnLAN = $true  
           } elseif ($ErrCode -eq 119) {  
                $WakeOnLAN = $true  
           } else {  
                $WakeOnLAN = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $WakeOnLAN = $true  
      }  
      Remove-Variable -Name WakeOnLANSetting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --deepsleepctrl=disable  
 #Test if deepsleepctrl exists on current system  
 If ($Commands -like '*deepsleepctrl*') {  
      [string]$DeepSleepCtrlSetting = 'deepsleepctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --deepsleepctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $DeepSleepCtrlSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $DeepSleepCtrlSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $DeepSleepCtrl = $true  
           } elseif ($ErrCode -eq 119) {  
                $DeepSleepCtrl = $true  
           } else {  
                $DeepSleepCtrl = $false  
           }  
           Remove-Variable -Name ErrCode  
      }  
      Remove-Variable -Name DeepSleepCtrlSetting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --blocks3=disable  
 #Test if blocks3 exists on current system  
 If ($Commands -like '*blocks3*') {  
      [string]$BlockS3Setting = 'blocks3=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --blocks3} -ErrorAction SilentlyContinue  
      If ($Output -ne $BlockS3Setting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $BlockS3Setting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $BlockS3 = $true  
           } elseif ($ErrCode -eq 119) {  
                $BlockS3 = $true  
           } else {  
                $BlockS3 = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $BlockS3 = $true  
      }  
      Remove-Variable -Name BlockS3Setting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --cstatesctrl=disable  
 #Test if cstatesctrl exists on current system  
 If ($Commands -like '*cstatesctrl*') {  
      [string]$CStateCTRLSetting = 'cstatesctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --cstatesctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $CStateCTRLSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $CStateCTRLSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $CStateCTRL = $true  
           } elseif ($ErrCode -eq 119) {  
                $CStateCTRL = $true  
           } else {  
                $CStateCTRL = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $CStateCTRL = $true  
      }  
      Remove-Variable -Name CStateCTRLSetting  
      Remove-Variable -Name Output  
 }  
 ##Disable Energy Efficient Ethernet  
 #Energy Efficient Ethernet disable registry value  
 $RegistryValue = '0'  
 #Find ethernet adapter  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Efficient Ethernet*'}).DisplayName  
 #Test for presence of Energy-Efficient Ethernet  
 If ($DisplayName -like '*Efficient Ethernet*') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           Set-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName -RegistryValue $RegistryValue  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($RegistryValue -eq $CurrentState) {  
                $EnergyEfficientEthernet = $true  
           } else {  
                $EnergyEfficientEthernet = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $EnergyEfficientEthernet = $true  
      }  
      Remove-Variable -Name RegistryValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Enable Wake on Magic Packet  
 $State = 'Enabled'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Magic Packet*'}).DisplayName  
 #Test if Magic Packet exists  
 If ($DisplayName -like '*Magic Packet*') {  
      [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
      If ($CurrentState -ne $State) {  
           Set-NetAdapterPowerManagement -Name $Adapter -WakeOnMagicPacket $State  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($State -eq $CurrentState) {  
                $WakeOnMagicPacket = $true  
           } else {  
                $WakeOnMagicPacket = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $WakeOnMagicPacket = $true  
      }  
      Remove-Variable -Name State  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Disable Shutdown Wake-On-Lan  
 $RegistryValue = '0'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -eq 'Shutdown Wake-On-Lan'}).DisplayName  
 If ($DisplayName -eq 'Shutdown Wake-On-Lan') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           Set-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName -RegistryValue $RegistryValue  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($RegistryValue -eq $CurrentState) {  
                $ShutdownWakeOnLAN = $true  
           } else {  
                $ShutdownWakeOnLAN = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $ShutdownWakeOnLAN = $true  
      }  
      Remove-Variable -Name RegistryValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Enable Allow the computer to turn off this device  
 $KeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\'  
 #Test if KeyPath exists  
 If ((Test-Path $KeyPath) -eq $true) {  
      $PnPValue = 256  
      $Adapter = Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}  
      foreach ($Entry in (Get-ChildItem $KeyPath -ErrorAction SilentlyContinue).Name) {  
           If ((Get-ItemProperty REGISTRY::$Entry).DriverDesc -eq $Adapter.InterfaceDescription) {  
                $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                If ($Value -ne $PnPValue) {  
                     Set-ItemProperty -Path REGISTRY::$Entry -Name PnPCapabilities -Value $PnPValue -Force  
                     Disable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false  
                     Enable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false  
                     $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                }  
                If ($Value -eq $PnPValue) {  
                     $PowerManagement = $true  
                } else {  
                     $PowerManagement = $false  
                }  
                Remove-Variable -Name Value  
           }  
      }  
      Remove-Variable -Name PnPValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name KeyPath  
 }  
 ##Disable Fast Startup  
 $KeyPath = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Power"  
 #Test if KeyPath exists  
 If ((Test-Path -Path ('REGISTRY::' + $KeyPath)) -eq $true) {  
      Set-ItemProperty -Path ('REGISTRY::' + $KeyPath) -Name 'HiberbootEnabled' -Value 0  
      If ((Get-ItemProperty -Path ('REGISTRY::' + $KeyPath)).HiberbootEnabled -eq 0) {  
           $FastStartup = $false  
      } else {  
           $FastStartup = $true  
      }  
 }  
 #Write-Host 'Wake-On-LAN:'$WakeOnLAN  
 #Write-Host 'Deep Sleep Control:'$DeepSleepCtrl  
 #Write-Host 'BlockS3:'$BlockS3  
 #Write-Host 'CState Control:'$CStateCTRL  
 #Write-Host 'Energy Efficient Ethernet:'$EnergyEfficientEthernet  
 #Write-Host 'Wake-On-Magic-Packet:'$WakeOnMagicPacket  
 #Write-Host 'Shutdown Wake-On-LAN:'$ShutdownWakeOnLAN  
 #Write-Host 'Allow Computer to Turn Off this Device:'$PowerManagement  
 If ((($WakeOnLAN -eq $null) -or ($WakeOnLAN -eq $true)) -and ($FastStartup -eq $false) -and (($DeepSleepCtrl -eq $null) -or ($DeepSleepCtrl -eq $true)) -and (($BlockS3 -eq $null) -or ($BlockS3 -eq $true)) -and (($CStateCTRL -eq $null) -or ($CStateCTRL -eq $true)) -and (($EnergyEfficientEthernet -eq $null) -or ($EnergyEfficientEthernet -eq $true)) -and (($WakeOnMagicPacket -eq $null) -or ($WakeOnMagicPacket -eq $true)) -and (($ShutdownWakeOnLAN -eq $null) -or ($ShutdownWakeOnLAN -eq $true)) -and (($PowerManagement -eq $null) -or ($PowerManagement -eq $true))) {  
      echo $true  
 } else {  
      echo $false  
 }  
   


Finally, the compliance rule is as follows:


You may wonder why I included Remove-Variable cmdlets. I used those when I was debugging so it was easier to track variable values.

For the configuration baseline, I have it configured as shown below:


11 April 2019

Ensuring Compliance When Deploying a Self-Updating Application

In my list of recent security projects, I needed to ensure certain applications are present on systems by using SCCM application deployment. One of those applications was Dell Command | Configure. The issue with this application is the Dell Command | Update will update the application which in turn would register it as not installed to SCCM, thereby kicking off the installation again. That, in turn, would downgrade the application. There are three built-in options in SCCM to choose from that indicate whether an application is installed or not. Those are application GUID, files, and registry. The GUID typically changes every time an app is upgraded and the files and registry can change too. Luckily, this application never changes its name in the programs and features. The version field is typically what changes unless it is a significant upgrade.

The fourth option for confirming if an app is installed is custom method detection where you use a PowerShell script. That is the option I have used to make sure the Dell Command | Configure is registered as installed, no matter the version it has updated to. The following script can be used for this purpose. As you can see, I assigned the application name exactly as it appears in the programs and features to the variable $Application. If a company does include the version in the application name, then you can wildcard the version portion. Say the example below was Dell Command | Configure 3.1, you could use Dell Command | Configure for $Application and it would still find the app. You might wonder why I am outputting the name of the application. All SCCM wants to see is a string output which it interprets as installed. If no output occurs, then SCCM interprets that as not installed. 


 $Application = 'Dell Command | Configure'  
 $InstalledApps = Get-ChildItem -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse | ForEach-Object {$_.GetValue('DisplayName')}  
 If (@($InstalledApps) -like ('*' + $Application + '*')) {  
      Write-Host (@($InstalledApps) -like $Application)  
 }  

01 April 2019

Running an SCCM Package via PowerShell and Command Line

While working on a new compliance policy, I ran into a lot of hurdles that needed to be resolved. One of those hurdles was executing an SCCM package via PowerShell. Using the WMIExplorer, I was able to locate a method that allows you to execute an SCCM package as shown below.


Once I located the namespace, class, and method, I needed to find out the name of the package in SCCM that I wanted to execute. To do this, the easiest method is to perform a WMI query in PowerShell on the advertised machine. The query is:

 Get-WmiObject -Class ccm_program -Namespace root\ccm\ClientSDK  

The results will display all advertised programs to that machine. From the results, locate the package you are wanting to execute by the Name, which will match the name in the SCCM console. Once you have found the package, take note of the PackageID and ProgramID, as these are the two items needed to execute the package via PowerShell.

The syntax of calling this from PowerShell is as follows, where the Program ID and Package ID are substituted with the appropriate data from the WMI query:

 ([wmiclass]'root\ccm\ClientSDK:CCM_ProgramsManager').ExecuteProgram('<ProgramID>','<Package ID>')  

The following PowerShell command line method will allow you to call this from the command line, where the Program ID and Package ID are substituted with the appropriate data from the WMI query:

 powershell.exe -executionpolicy bypass -command "&{([wmiclass]'root\ccm\ClientSDK:CCM_ProgramsManager').ExecuteProgram('<ProgramID>','<Package ID>')}"  

When executing the package, there will be output. If you allow for the default output, it can take an extended period of time to gather the information. Specifying a specific field will dramatically speed up the execution time.

NOTE: One thing I learned after discovering this is that SCCM Compliance rules cannot execute this WMI method. I will be writing a separate blog on that in the future as I just finished the compliance policy that goes into detail on executing an SCCM package. 

27 March 2019

Initiating an SCCM Compliance Check via PowerShell

Recently, I have been working on Configuration Baselines for security purposes. While doing so, two of my baselines required remediation that takes longer than 1 minute. I do not recall where I read it, but I believe the timeout for a compliance check is 1 minute. If the compliance remediation takes longer than 1 minute, then the baseline is designated as non-compliant until the next compliance check is run. This snippet of code can also be used in any other instance where the configuration manager client is installed.

To expedite this process, I tracked down how to execute a compliance check through PowerShell so that it can be executed at the end of the remediation script. Thanks to Trevor Sullivan's blog post, I was able to grab and modify the code from it to make into an easy to use code snippet within a Baseline remediation PowerShell script.

To make this easier, I wrote the script as two lines. The first line is where you specify the name of the baseline. As you can see in the pic below of a partial list of baseline configurations, the names of those baselines are what you need to specify for the variable $Name. The code snippet at the bottom shows using the Pending Reboot name to trigger a compliance check for that baseline. 



Once you have specified the name of the baseline, you can then copy and paste both lines at the bottom of the PowerShell remediation script so that a baseline configuration is triggered at the end of the remediation. Here is the code snippet: 


 $Name='Pending Reboot'  
 ([wmiclass]"root\ccm\dcm:SMS_DesiredConfiguration").TriggerEvaluation(((Get-WmiObject -Namespace root\ccm\dcm -class SMS_DesiredConfiguration | Where-Object {$_.DisplayName -eq $Name}).Name), ((Get-WmiObject -Namespace root\ccm\dcm -class SMS_DesiredConfiguration | Where-Object {$_.DisplayName -eq $Name}).Version))  
   

08 March 2019

Bitlocker Active Directory Recovery Password Backup Compliance

Recently, we had an issue of some machines not backing up the Bitlocker recovery password to active directory, even with the GPO in place. They ended up being offline while the bitlocker process took place. Plus, some of the systems in AD had multiple entries, which can be cumbersome. To mitigate this issue, I have implemented an SCCM Configuration Baseline that makes sure the Bitlocker recovery password is backed up to AD and that it is the only recovery password present.

NOTE: This script is being used in an environment that only encrypts the %systemdrive%. If your environment encrypts other items such as flash drives, removable HDDs, and etc, you will need to modify these scripts to meet your environment needs. It will delete those items from active directory also. 

To do this, I first implemented a baseline that enabled the RSAT active directory feature in Windows 10. This is needed so the scripts can query and write to AD. Once this was deployed, I created the BitLocker Recovery Password Backup configuration item.


 Platforms must be set to Windows 10 as some of the cmdlets used in the scripts only exist in that OS and newer.



The script returns a true or false value that dictates if remediation is needed.


The first script queries the local system and AD for the recovery passwords to compare. If they match and only one is in AD, then True is returned that dictates the system is in compliance. False is returned if there is no password stored in AD, there is more than one password in AD, or the wrong password is stored in AD.

Here is the discovery script:

 $RecoveryKey = (Get-BitLockerVolume -MountPoint $env:SystemDrive).KeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}  
 $ADBitLockerRecoveryKey = (Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase (Get-ADComputer -Identity $env:COMPUTERNAME).DistinguishedName -Properties 'msFVE-RecoveryPassword')  
 If ($ADBitLockerRecoveryKey -eq $null) {  
      Echo $false  
 } elseif ($ADBitLockerRecoveryKey -isnot [system.Array]) {  
      If (([string]$RecoveryKey.RecoveryPassword).Trim() -eq ([string]$ADBitLockerRecoveryKey.'msFVE-RecoveryPassword').Trim()) {  
           Echo $true  
      } else {  
           Echo $false  
      }  
 } elseif ($ADBitLockerRecoveryKey -is [system.Array]) {  
      Echo $false  
 }  


Next comes the remediation script. This is what will be executed if the discovery script returns a False value:



 $RecoveryKey = (Get-BitLockerVolume -MountPoint $env:SystemDrive).KeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}  
 Write-Host 'Local Recovery Password:'$RecoveryKey.RecoveryPassword  
 $ADBitLockerRecoveryKey = (Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase (Get-ADComputer -Identity $env:COMPUTERNAME).DistinguishedName -Properties 'msFVE-RecoveryPassword')  
 Write-Host '  AD Recovery Password:'$ADBitLockerRecoveryKey.'msFVE-RecoveryPassword'  
 If (($ADBitLockerRecoveryKey -isnot [system.Array]) -and ($ADBitLockerRecoveryKey -ne $null)) {  
      Remove-ADObject -Identity $ADBitLockerRecoveryKey.DistinguishedName -Confirm:$false  
 } elseif ($ADBitLockerRecoveryKey -is [system.Array]) {  
      Foreach ($Key in $ADBitLockerRecoveryKey) {  
           Write-Host 'Removing'$Key.DistinguishedName  
           Remove-ADObject -Identity $Key.DistinguishedName -Confirm:$false  
      }  
 }  
 Backup-BitLockerKeyProtector -MountPoint $env:SystemDrive -KeyProtectorId $RecoveryKey.KeyProtectorId  
 

The final thing to set in the configuration item is the compliance rule as shown below:


Now that the configuration item is created, the configuration baseline must be created and deployed. Here are the screenshots of my configuration baseline that I later deployed out to all laptop systems, which are the systems here that are BitLockered.





07 March 2019

Active Directory PowerShell Module Configuration Baseline

With the recent 1809, RSAT is now integrated into Windows, which is a major plus for the admin side. In my environment, I have the active directory PowerShell module enabled on all machines for two reasons. The first is I use it to move the machine in AD during the build process. The second is that I have an SCCM baseline that makes sure the Bitlocker key matches the one stored in AD. For these, I need the module installed and thankfully it is now just a simple Add-WindowsCapability cmdlet.

I implemented the following baseline that first checks to make sure the Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 feature is enabled. It returns a boolean value of $true if it is Installed and $false if it is Not Present. If $false is returned, then the remediation script will turn on the feature. 

I am going to assume you already know how to setup a configuration item, so I am not going to go through the screen by screen process. This is the main screen of the Item. 



Here is the PowerShell query for checking if it is enabled and returning the $true or $false. 

 If ((Get-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0').State -eq 'Installed') {Echo $true} elseif ((Get-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0').State -eq 'NotPresent') {echo $false}  



Here is the remediation script for enabling RSAT AD if it is not enabled.

 Add-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0'  


Finally, this is the compliance rule that enables the remediation if it is not enabled.


Now to deploy the Configuration Item, the Baseline needs to be created and deployed. This is a very simple procedure. Here are screenshots of my setup of the Baseline.



04 March 2019

Local Administrator Baseline Compliance

One of the issues we have had is some users ending up being in the administrators group. There are circumstances to which we have to sometimes put a user in that group to install an application which is both user and computer-based. It can be easy to forget to take the user back out of that group. We don't allow the end users here to have local administrator privileges for security reasons.

I have finally gotten around to using PowerShell along with the compliance settings in SCCM to manage this issue. To implement a compliance setting to monitor systems where users have local admin privs, I first setup the configuration item. As shown below, I setup the configuration item to look for an integer value, 0 or 1, returned from the PowerShell script. It returns 0 if nothing shows up in the query and a 1 if there are users in the query.

The first step is to create the Configuration Item as shown in the following instructions:

In SCCM under Assets and Compliance-->Compliance Settings-->Configuration Items, click Create Configuration Item from the toolbar above.


In my environment, we are now only Windows 10, so I selected that as the platform.


The next screen will be to create the conditions associated with the configuration item. Under this, you will click New


The next screen is creating the setting. I used the name Local Administrators, the setting is defined by a PowerShell script that returns an integer value of 0 or 1.


The next screen is entering the PowerShell script to query for users that may exist in the group. If there are users in your environment that need to be there by default, you will need to add them to the where-object to exclude from the query. You could also put them in a text file on a UNC share the script could read and compare against.


Here is the script for easy copy and paste.

 If ((Get-LocalGroupMember -Group Administrators | Where-Object {($_.ObjectClass -eq 'User') -and ($_.Name -notlike '*Administrator*')}).Count -gt 0) {Echo 1} else {Echo 0}  

Next comes the Compliance Rules. This is where you specify what the value returned from the query is considered as complying.


This is the specification defined for the rule.



Now that Configuration Items is created, we must create a Baseline that will use the Configuration Item when deployed out to collections.

In SCCM under Assets and Compliance-->Compliance Settings-->Configuration Baselines, click Create Configuration Baseline from the toolbar above.


I used the Name Local Admin. Next, click on Add-->Configuration Items. The following screen will appear:


Select the Local Administrator and click Add. Mine in the pic is slightly different in naming because I already had this created before writing this blog.

Now click OK and the Configuration Baseline will be created. The Baseline is now ready to be deployed out. Select the Local Admin Baseline from the Configuration Baselines and click Deploy. The following screen will appear:


These are the specifications I decided to use. I made the alert to generate if 100% compliance is not met so I know by the next day if someone has local admin. As you can see in the results below, the system I deployed it to is compliant.


I also went into that system and added a user to the administrators group it returned the result of non-compliant when I reran the compliance scan. Another thing that can be done here is to create collections that are based on the compliance and non-compliance of the baseline. This can be done by clicking on the configuration baseline and then right-clicking on the deployment at the bottom. Click on Create New Collection and the options to create the collections by the results will come up as shown below.


If you want to expedite the evaluation time while testing this out, you can go to a system you have deployed this to and open up Configuration Manager from the control panel. Under that, click configurations. If it is not appearing there yet, click refresh and the new baseline should appear. Now that it is displayed there, you can click evaluate at the bottom to run the baseline.


15 February 2019

Loss of Bluetooth Connectivity Resolved via PowerShell

Recently, we ran into the issue of users replacing their keyboard and mouse with Bluetooth devices. What happened was they would lose connectivity and the error below would appear in the event viewer logs.


While researching the issue, we found that the user could open up the laptop that was docked and get connectivity back by hitting any key. The culprit was the Power Management setting of the Bluetooth device. The "Allow the computer to turn off this device to save power" setting disconnected the Bluetooth devices, and because both were the keyboard and mouse, there was no way for them to wake Bluetooth back up. The fix for this was to uncheck the setting as shown below. 


At first, I thought because this was a similar setting as is on the NIC Power Management that I could manipulate it via the registry. I could not find any key that configures this setting. Through more research, I was able to find this solution in a posting on Reddit

I took the script and made it into a one-liner with the Enable variable set to $false in the beginning since it is enabled ($true) by default.  This will allow for the script to be implemented inside a task sequence in MDT or SCCM. 

Here is the script in both a one-liner and regular code. 

PowerShell One-Liner if you want to use this in an MDT or SCCM command line task sequence:
 powershell.exe -executionpolicy bypass -command "&{$Enable=$false;$BTDevice=Get-PnpDevice -Class Bluetooth -InstanceId USB*;$BTDevice | ForEach-Object -Process {$WQL='SELECT * FROM MSPower_DeviceEnable WHERE InstanceName LIKE ' + [char]34 + [char]37 + $([Regex]::Escape($_.PNPDeviceID)) + [char]37 + [char]34;Set-CimInstance -Namespace root\wmi -Query $WQL -Property @{Enable = $Enable} -PassThru};Get-PnpDevice -Class Bluetooth -InstanceId USB* | ForEach-Object -Process {$Test='InstanceName LIKE ' + [char]34 + [char]37 + $([Regex]::Escape($_.PNPDeviceID)) + [char]37 + [char]34;If ((Get-CimInstance -ClassName MSPower_DeviceEnable -Namespace root\wmi -Filter $Test).Enable -eq $Enable) {Write-Host 'Success';Exit 0} else {Write-Host 'Failed';Exit 1}}}"  

PowerShell .PS1 File

 <#  
      .SYNOPSIS  
           Bluetooth Power Management  
        
      .DESCRIPTION  
           This script will enable or disable the Power Management Setting that allows the computer to turn off the Bluetooth device to save power  
        
      .PARAMETER Enable  
           $true will check the Allow the computer to turn off this device to save power. $false will do the opposite. The default has been set to $false since it is originally checked in the OS  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:      2/12/2019 3:20 PM  
           Created by:      Mick Pletcher  
           Filename:        BluetoothPowerState.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      $Enable = $false  
 )  
   
 #$Enable = $false  
 $BTDevice = Get-PnpDevice -Class Bluetooth -InstanceId USB*; $BTDevice | ForEach-Object -Process {  
      $WQL = 'SELECT * FROM MSPower_DeviceEnable WHERE InstanceName LIKE ' + [char]34 + '%' + $([Regex]::Escape($_.PNPDeviceID)) + '%' + [char]34  
      Set-CimInstance -Namespace root\wmi -Query $WQL -Property @{  
           Enable = $Enable  
      } -PassThru  
 }  
 Get-PnpDevice -Class Bluetooth -InstanceId USB* | ForEach-Object -Process {  
      $Test = 'InstanceName LIKE ' + [char]34 + '%' + $([Regex]::Escape($_.PNPDeviceID)) + '%' + [char]34  
      If ((Get-CimInstance -ClassName MSPower_DeviceEnable -Namespace root\wmi -Filter $Test).Enable -eq $Enable) {  
           Write-Host $BTDevice.FriendlyName'Power Management Successfully Configured'  
           Write-Host  
           Exit 0  
      } else {  
           Write-Host $BTDevice.FriendlyName'Power Management Failed to Configure'  
           Exit 1  
      }  
 }  
   

04 February 2019

Default Printer Report

When our build team builds new machines for users, we provide a convenience to the user of letting them know what their default printer is. I wrote this script that will parse through all user profiles in HKU to find the default printer of each profile. It will then write the results to the screen if the script is manually executed, while also writing to the DefaultPrinter.CSV file located at the default directory of each user profile. This allows for the script to be deployed through SCCM, or be executed remotely or locally with PowerShell. The reason I have it write to a CSV file instead of reporting to SCCM is that not everyone has access to SCCM and for universal compatibility, not all companies have SCCM.

I deployed this script through a package in SCCM that is scheduled to execute once a week. Every time this script executes, it will replace the current file with a new one.

You can download the script from here.


 <#  
      .SYNOPSIS  
           Default Printer Report  
        
      .DESCRIPTION  
           This script will retrieve a list of all user profiles and report to a text file inside each user profile what the default printer is.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       2/4/2019 8:56 AM  
           Created by:       Mick Pletcher  
           Filename:         DefaultPrinterReport.ps1  
           ===========================================================================  
 #>  
   
 [CmdletBinding()]  
 param ()  
   
 $Profiles = (Get-ChildItem -Path REGISTRY::HKEY_USERS -Exclude *Classes | Where-Object {$_.Name -like '*S-1-5-21*'}).Name  
 $ProfileArray = @()  
 foreach ($Item in $Profiles) {  
      $object = New-Object -TypeName System.Management.Automation.PSObject  
      $object | Add-Member -MemberType NoteProperty -Name Profile -Value ((Get-ItemProperty -Path ('REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\' + ($Item.split('\')[1].Trim())) -Name ProfileImagePath).ProfileImagePath).Split('\')[2]  
      $object | Add-Member -MemberType NoteProperty -Name DefaultPrinter -Value ((Get-ItemProperty -Path ('REGISTRY::' + $Item + '\Software\Microsoft\Windows NT\CurrentVersion\Windows') -Name Device).Device).Split(',')[0]  
      $ProfileArray += $object  
 }  
 $ProfileArray
 foreach ($Item in $ProfileArray) {  
      Export-Csv -InputObject $Item -Path ($env:SystemDrive + '\users\' + $Item.Profile + '\DefaultPrinter.csv') -NoTypeInformation -Force  
 }  
   

01 February 2019

Mozilla Firefox One-liner Installer

Here is a PowerShell one-line installer for Mozilla Firefox. This allows you to download the latest version of Mozilla Firefox during the build process without having to maintain the package each time. The URI used in this is for the 64-bit version of Firefox. If you need a different version, you will need to locate the download URI and copy and paste it in the one-liner thereby changing the value of $URI. I have been using this one-liner to install Firefox in the build for almost a year.

This one-liner includes error checking so in the event the URI is no longer valid, you will be alerted to it and be able to update the script with the new URI.


 powershell.exe -executionpolicy bypass -command "&{$URI='https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US';$AppInstaller=$env:TEMP+'\'+'Firefox.exe';Invoke-WebRequest -Uri $URI -OutFile $AppInstaller -ErrorAction SilentlyContinue;$ErrCode=(Start-Process -FilePath $AppInstaller -ArgumentList '/silent /install' -Wait -Passthru).ExitCode;Remove-Item $AppInstaller -ErrorAction SilentlyContinue -Force;Exit $ErrCode}"  

Deploying Ping Automated Timekeeping for Lawyers

This application is straightforward to deploy. The PowerShell script below will kill all processes associated with Microsoft Office. Ping requires closing Outlook, but I have seen in the past where other Office apps can also interfere by keeping a component of Outlook open, so to be on the safe side, I included closing the entire suite, along with closing Ping if it is already installed.

I designed the script to first kill the necessary processes. Next, it will search for previously installed versions of Ping and uninstall them. I include this for two reasons. First, if the application is installed, but is busted, rerunning the installer will uninstall it and reinstall it as a fix. A lot of times, it is much faster to do this than to go through an entire session of troubleshooting that often comes back to this. Second, if there is an upgrade, this will uninstall the old version. The uninstaller is designed to be able to query the add/remove programs in the registry for an application that matches the name used. Next, the two Ping components are installed. I have also included the uninstaller, which is the same script without the two application installs. 

You can download both from my GitHub site by clicking on the links below:

23 January 2019

PowerShell One-Liner to Configure the NIC Power Management Settings

While working on a series of one-liners for configuring the NIC on machines, I created this needed to makes changes to the power management settings of the NIC. This is something that will be implemented in the build, so I also wanted to make the script into a one-liner so the code itself can reside within the task sequence.

This one-liner can check/uncheck the boxes within the Power Management tab of the network adapter. There are two ways this can be done. The first is by WMI and the second is by the registry. The WMI method failed across the different versions of Windows 10, whereas the registry method stays the same. 



The script first finds the correct NIC by querying for the one that is enabled and is also a physical NIC. Next, it locates the correct registry key for that NIC by comparing the driver names. It then checks the PnPCapabilities value. The value associated with this entry determines which boxes are checked/unchecked. It will then check if the registry has the same value as is specified in the script. If not, it will change the registry value, disable, and enable the NIC, and retrieve the registry value. It will then compare that value with the specified $PnPValue to verify the change took place. If it did not, the script will exit with an error code 1, thereby reporting to SCCM/MDT that it failed. 

There are four values that can be used to change the boxes. The value is associated with the $PnPValue variable. The only thing you should have to change with this one-liner is the $PnPValue, which is why I placed that in the front of the one-liner for easy editing. The $PnP values are as follows:

  • 0 - Checks both Allow the computer to turn off this device to save power and Allow this device to wake the computer, while leaving Only allow a magic packet to wake the computer unchecked
  • 24 - Unchecks all three boxes
  • 256 - Checks all three boxes
  • 272 - Checks Allow Allow the computer to turn off this device to save power, while unchecking Allow this device to wake the computer and Only allow a magic packet to wake the computer

NOTE: I did learn one interesting thing while writing and testing this. The WMI method will disable and enable the NIC when making the change, whereas, with the registry method, you must disable and enable the NIC separately as is done in the one-liner below.

Here is the one-liner:

 powershell.exe -executionpolicy bypass -command "&{$PnPValue=256;$Adapter=Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')};$KeyPath='HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\';foreach ($Entry in (Get-ChildItem $KeyPath -ErrorAction SilentlyContinue).Name) {If ((Get-ItemProperty REGISTRY::$Entry).DriverDesc -eq $Adapter.InterfaceDescription) {$Value=(Get-ItemProperty REGISTRY::$Entry).PnPCapabilities;If ($Value -ne $PnPValue) {Set-ItemProperty -Path REGISTRY::$Entry -Name PnPCapabilities -Value $PnPValue -Force;Disable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false;Enable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false;$Value=(Get-ItemProperty REGISTRY::$Entry).PnPCapabilities};If ($Value -eq $PnPValue) {Write-Host 'Allow the computer to turn off this device is configured'} else {Write-Host 'Failed';Exit 1}}}}"  

09 January 2019

PowerShell One-Liner to Enable Features in Microsoft Windows 1809

In Windows 10 1809, I needed to enable some RSAT features that are now included in the OS. I figured this would be a good time to go from using a script to using one-liners for the build process. Mike Robbins's blog was a good start to developing this one-liner. This allows for you to manage the code within the task sequence, thereby negating the issue of storing a script and the possibility of the script accidentally being deleted.

NOTE: This script will only execute in the Microsoft Windows 1809. It will not run in 1803 or older.

The one-liner below enables a feature defined in the $Name variable. The $Name variable is used to make it easy to define what feature to enable by having it right at the front of the one-liner. Once that is defined, and the script is executed, it will check to make sure the feature is enabled and returns an error code 0, along with an "Enabled" message in the event the script is manually executed, or it returns an error code 1 if it failed to enable along with the message "Failed to Enable." I did verify that it will still report enabled, even if a system reboot is required.

When defining the $Name variable, you do not need to use the full feature name. For instance, the Active Directory feature's name is Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0. If there is no other feature with the name ActiveDirectory in it, you can use a wild card like this:

  • $Name = 'Rsat.ActiveDirectory*'
NOTE: You see that I use apostrophes in the script. That is because of the quotation marks before the & and at the end of the script. 

Here is the one-liner script that I use to enable AD. You just need to change the $Name variable to whatever feature you want to be activated. 

 powershell.exe -executionpolicy bypass -command "&{$Name='Rsat.ActiveDirectory*';Get-WindowsCapability -name $Name -Online | Add-WindowsCapability -Online;If ((Get-WindowsCapability -name $Name -Online).State -eq 'Installed') {Write-Host 'Enabled';Exit 0} else {Write-Host 'Failed to Enable';Exit 1}}"  

Copy and paste the script into a command line task sequence as shown below.


01 November 2018

Upgrading Microsoft Orchestrator from 2012 and 2016

It was time for us to upgrade the Microsoft Orchestrator to the newest 1801 version. We were three versions behind as we have been using 2012. Luckily starting with 1801, upgrades are performed via windows updates.

Microsoft provides a well-documented page on setting up Orchestrator located here. The problem with upgrading from Orchestrator 2012 and 2016 is the fact that you must uninstall the old version and reinstall the new version. The SQL server Orchestrator was connected to had not been documented in the beginning. We could find nothing in the console on what it was connected to. The registry was useless and the event viewer logs did not help. We started going through the Orchestrator logs located at %Programdata%\Microsoft System Center 2012\Orchestrator\. Each subdirectory has a logs folder. We finally located the log that contained SQL server and instance Orchestrator was connecting to. The log is located at %ProgramData%\Microsoft System Center 2012\Orchestrator\ManagementService.exe\Logs. You will likely have to go through each log in that directory to find the SQL server. The line will look like this:

  • <Param>App=<Instance>;Provider=SQLOLEDB;Data Source=<SQLServer>\<Database>;Initial Catalog=Orchestrator;Integrated Security=SSPI;Persist SecurityInfo=False;</Param>
Once we found the SQL server information, we were able to successfully upgrade the server to 1801 using the classical uninstall/reinstall, and then onto 1809 via windows updates. 

24 October 2018

User Logon Reporting

If you have to track the login times for a specific user, this tool will generate a report for you that scans the event viewer logs for ID 4624. The tool parses each event and retrieves the user name, securityID, type of logon, computer name, and time stamp. It formats the output and writes it to a centralized CSV file in the event this tool is deployed to multiple machines at once. The tool has the ability to 'wait for its turn' to write to the file when it is deployed to multiple systems.

I have the script translate what each of the logon types is. If you do not want a specific logon type to be reported, you can comment out that type within the switch cmdlet and it will not appear in the report.

NOTE: I originally wrote this script to have Get-WinEvent remotely execute on a machine using the -computer parameter and the time required was huge, especially on older systems with three months plus of event viewer data. It took almost 30 minutes. It ended up being much quicker to deploy the script via an SCCM package.

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


 <#  
      .SYNOPSIS  
           Logon Reporting  
        
      .DESCRIPTION  
           This script will report the computername, username, IP address, and date/time to a central log file.  
        
      .PARAMETER LogFile  
           A description of the LogFile parameter.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/22/2018 10:13 AM  
           Created by:       Mick Pletcher  
           Filename:         LogonReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$LogFile = 'LogonReport.csv'  
 )  
   
 $Entries = @()  
 $IPv4 = foreach ($ip in (ipconfig) -like '*IPv4*') {($ip -split ' : ')[-1]}  
 $DT = Get-Date  
 foreach ($IP in $IPv4) {  
      $object = New-Object -TypeName System.Management.Automation.PSObject  
      $object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME  
      $object | Add-Member -MemberType NoteProperty -Name UserName -Value $env:USERNAME  
      $object | Add-Member -MemberType NoteProperty -Name IPAddress -Value $IP  
      $object | Add-Member -MemberType NoteProperty -Name DateTime -Value (Get-Date)  
      $object  
      $Entries += $object  
 }  
 foreach ($Entry in $Entries) {  
      Do {  
           Try {  
                Export-Csv -InputObject $Entry -Path $LogFile -Encoding UTF8 -NoTypeInformation -NoClobber -Append  
                $Success = $true  
           } Catch {  
                $Success = $false  
                Start-Sleep -Seconds 1  
           }  
      } while ($Success -eq $false)  
 }  
   

17 October 2018

PowerShell One-Liners to ensure Dell system is configured for UEFI when imaging

While planning and configuring the Windows 10 upgrades, we had to also include the transition to UEFI from BIOS. I wanted to make sure that when the build team builds new models that they are configured for UEFI when applicable, otherwise the build fails within seconds after it starts.

We use Dell systems, so interacting with the BIOS is simple. The Dell Command | Configure allows for the BIOS to be queried, which is what we need here to verify specific models are set correctly. We do have a few models that are not compatible with UEFI, so those have to be exempted. In looking at Dell Latitude models, anything newer than the E6320 is compatible with UEFI. Granted, there may be other models that we never had that could be compatible.

There are four key settings in the BIOS that determine if a system is compatible with UEFI. Those settings are the Boot List Option, Legacy Option ROMs, UEFI Network Stack, and Secure Boot. I have found the most reliable one of the four to verify compatibility is the UEFI Network Stack. If a system does not have this option, then UEFI is not compatible.

I set this up as four task sequences within a folder called Verify UEFI. The folder performs two WMI queries to make sure it is a Dell machine, and it is not one of the five models we still have in production that are not UEFI compatible. The conditions are set up as shown in the screenshot below.


The first WMI query makes sure the system is a Dell.

  • select * from Win32_ComputerSystem WHERE Manufacturer like "%Dell%"

The second WMI query makes sure the system is not one of the specified models that are not compatible with UEFI.

  • select * from Win32_ComputerSystem WHERE (model != "Latitude E6320") and (model != "Latitude E6410") and (model != "Optiplex 980") and (model != "Optiplex 990") and (model != "Optiplex 9010")
This is the setup in MDT that I have configured

Now that the folder is set up, you will need to create each of the four Run Command Line task sequences. Before doing this, you will need to have Dell Command | Configure installed and loaded into the WinPE environment. You can refer to my blog posting that details how to load this into WinPE. 

Each one of the four tests is a Run Command Line. They will look like the pic below. All you will need to do is to copy the PowerShell one-liner code below and paste it into the command line of each task sequence.

Here is the PowerShell one-liner code for each task sequence:
  • Boot List Option
    • powershell.exe -executionpolicy bypass -command "&{If ((x:\cctk\cctk.exe bootorder --activebootlist) -like '*uefi') {exit 0} else {exit 1}}"
  • Legacy Option ROMs
    • powershell.exe -executionpolicy bypass -command "&{If ((x:\cctk\cctk.exe --legacyorom) -like '*disable') {exit 0} else {exit 1}}"
  • UEFI Network Stack
    • powershell.exe -executionpolicy bypass -command "&{If ((x:\cctk\cctk.exe --uefinwstack) -like '*enable') {exit 0} else {exit 1}}"
  • Secure Boot
    • powershell.exe -executionpolicy bypass -command "&{If ((x:\cctk\cctk.exe --secureboot) -like '*enable') {exit 0} else {exit 1}}"
As you can see, if any of these fail, they will return an error code 1 and then fail the build.