Mick's IT Blogs

Mick's IT Blogs

Latest Updates

29 January 2016

Update Drivers and BIOS via PowerShell and Dell Command Update

Posted By: Mick Pletcher - 3:16 PM









NOTE: I used Sapien's PowerShell Studio to write this script that significantly simplified the process!

During the build process, I execute the Dell Command Update to make sure the latest drivers and BIOS version are installed. I use the script below so that it can disable the BIOS password and execute the dcu-cli.exe in either a 32-bit or 64-bit system. The BIOS password must be disabled if you want the BIOS to be updated. Also, I execute the script in two different task sequences. This allows for the BIOS to be completely updated. There are often interval BIOS updates that have to be installed first before the latest can be installed.

You can download the script from here.


 <#       
      .NOTES  
      ===========================================================================  
       Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
       Created on:       1/29/2016 2:59 PM  
       Created by:       Mick Pletcher  
       Organization:         
       Filename:        UpdateAll.ps1  
      ===========================================================================  
      .DESCRIPTION  
           Uses Dell Command | Update to update all drivers and BIOS versions.  
 #>  
   
 Function Set-ConsoleTitle {  
      Param ([String]$Title)  
      $host.ui.RawUI.WindowTitle = $Title  
 }  
   
 Function Get-Architecture {  
      #Declare Local Variables  
      Set-Variable -Name Architecture -Scope Local -Force  
        
      $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
      $Architecture = $Global:Architecture.OSArchitecture  
      #Returns 32-bit or 64-bit  
      Return $Architecture  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Architecture -Scope Local -Force  
 }  
   
 Function Install-Updates {  
      Param ([String]$DisplayName,  
           [String]$Executable,  
           [String]$Switches)  
        
      #Declare Local Variables  
      Set-Variable -Name ErrCode -Scope Local -Force  
        
      Write-Host "Install"$DisplayName"....." -NoNewline  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -Passthru).ExitCode  
      } else {  
           $ErrCode = 1  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 1) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name ErrCode -Scope Local -Force  
 }  
   
   
 Function CCTKSetting {  
      param ($Name,  
           $Option,  
           $Setting,  
           $Drives,  
           $Architecture)  
        
      #Declare Local Variables  
      Set-Variable -Name Argument -Scope Local -Force  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name EXE -Scope Local -Force  
        
      If ($Architecture -eq "32-bit") {  
           $EXE = $Env:PROGRAMFILES + "\Dell\Command Configure\X86\cctk.exe"  
      } else {  
           $EXE = ${env:ProgramFiles(x86)} + "\Dell\Command Configure\X86_64\cctk.exe"  
      }  
      If ($Option -ne "bootorder") {  
           $Argument = "--" + $Option + "=" + $Setting  
      } else {  
           $Argument = "bootorder" + [char]32 + "--" + $Setting + "=" + $Drives  
      }  
      Write-Host $Name"....." -NoNewline  
      If ((Test-Path $EXE) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $EXE -ArgumentList $Argument -Wait -Passthru).ExitCode  
      } else {  
           $ErrCode = 1  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 240) -or ($ErrCode -eq 241)) {  
           If ($Drives -eq "") {  
                Write-Host $Setting -ForegroundColor Yellow  
           } else {  
                Write-Host $Drives -ForegroundColor Yellow  
           }  
      } elseIf ($ErrCode -eq 119) {  
           Write-Host "Unavailable" -ForegroundColor Green  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Argument -Scope Local -Force  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name EXE -Scope Local -Force  
 }  
   
 #Declare Local Variables  
 Set-Variable -Name Architecture -Scope Local -Force  
 Set-Variable -Name EXE -Scope Local -Force  
   
 cls  
 Set-ConsoleTitle -Title "Dell Client Update"  
 $Architecture = Get-Architecture  
 CCTKSetting -Name "Disable BIOS Password" -Option "valsetuppwd" -Setting "<BIOS Password> --setuppwd=" -Drives "" -Architecture $Architecture  
 If ($Architecture -eq "32-bit") {  
      $EXE = $Env:PROGRAMFILES + "\Dell\CommandUpdate\dcu-cli.exe"  
 } else {  
      $EXE = ${env:ProgramFiles(x86)} + "\Dell\CommandUpdate\dcu-cli.exe"  
 }  
 Install-Updates -DisplayName "Update All Hardware Components" -Executable $EXE -Switches " "  
   
 #Cleanup Local Variables  
 Remove-Variable -Name Architecture -Scope Local -Force  
 Remove-Variable -Name EXE -Scope Local -Force  
   

28 January 2016

Validate TPM Settings at the Beginning of a Build of a Dell machine.

Posted By: Mick Pletcher - 2:42 PM














NOTE: I used Sapien's PowerShellStudio to write this script that significantly simplified the process!

I have wrestled with the issue of encrypting a system using Bitlocker during a build. The issue I have had is that not all of the process can be automated. The steps of turning on the TPM, and activating it can be automated. The portion where the TPM is cleared is a forced, manual process. If you put this in the build, then the build is stopped at the when the TPM is cleared upon the reboot waiting for a user to hit F12 to confirm the clearing of the TPM.

To get around this pause in the build, we decided to make the steps of turning on, clearing, and activating the TPM a manual process before the build process is started. I wrote the script below, integrating the BitLockerSAK function written by St├ęphane vg, to verify TPM is on, TPM is cleared, and TPM is activated. If it is not, the script pops up a window saying the build failed due to the TPM not being ready. It says to turn on the TPM, clear the TPM, and activate the TPM. When OK is clicked, the build fails. 

The function I wrote uses Dell's CCTK.exe file to check the status of the TPM. I have this executing in WinPE, before the operating system is laid down. To do this, you will need to run three command line tasks. 


The first will map the drive to the folder location of that contains the CCTK.exe. I arbitrarily selected T: as the drive letter to use. You enter:

net use t: \\<network location of CCTK.exe>\ /user:<domain>\username Password

The second task sequence is to install CCTK on the X: drive, which is the drive letter of the WinPE. The following command line should be exactly what you need to copy the files to x:\CCTK\:

 xcopy.exe "t:\*.*" "x:\CCTK\" /E /C /I /H /R /Y /V

The third step is to enable HAPI, which is also present in the X:\CCTK\ directory. The following command line should do this with no issues:

x:\CCTK\HAPI\hapint -i -k C-C-T-K -p X:\CCTK\HAPI\

Of course you will need to limit the task sequences to only machines which need to be bitlockered.

Now is the time to enter another task sequence that will execute the BitlockerSAK_CheckTPM.ps1 file. You will need to specify the executionpolicy since this is being executed in WinPE. The command line for this that I used was the following with the Start in also populated with the location of the script:

powershell.exe -executionpolicy bypass -file BitlockerSAK_CheckTPM.ps1

You can download the script from here.


1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
5:        Created on:       1/28/2016 1:55 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:         BitlockerSAK_CheckTPM.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will test if bitlocker is turned on, cleared, and   
12:            activated. It uses the BitLockerSAK function wirtten by St├ęphane vg to  
13:            check the ownership of Bitlocker. It also uses CCTK for testing  
14:            the status of TPM.  
15:  #>  
16:    
17:  Function BitLockerSAK {  
18:  <#  
19:  .SYNOPSIS  
20:       Get and set Bitlocker related information.  
21:      
22:  .DESCRIPTION  
23:       Based on WMI classes, this function can achiev the following tasks :  
24:    
25:    --TPM operations---  
26:      -TPM activation state.  
27:      -If the TPM is enabled or not.  
28:      -If a TPM OwnerShip is Allowed.  
29:      -If the TPM is currently owned.  
30:      -The possibility to take the ownerShip  
31:    
32:    --Encryption possibilities ---  
33:      - Retrieves the current encryption method.  
34:      - Get the current protection status.  
35:      - The current protection state.  
36:      - The possibility to encrypt a Drive.  
37:      - The possibility to Resume an encryption that has been paused.  
38:      - Possibility to return the current protector ID's.  
39:      - Possibility to return the current protector type(s).  
40:      - Retrieves the Volume key protector Passwords  
41:      
42:    
43:  .PARAMETER isTPMActivated  
44:       Returns activation state of the TPM:  
45:    Returns true if activated and false if not.  
46:    
47:  .PARAMETER isTPMEnabled  
48:       Returns the enabled state of the TPM:  
49:    Returns true if activated and false if not.  
50:    
51:  .PARAMETER IsTPMOwnerShipAllowed  
52:    Returns if the TPM ownership is allowed.  
53:    Returns true if allowed, false if not.  
54:    
55:  .PARAMETER ResumeEncryption  
56:    Will resume an paused encryption.  
57:    
58:  .PARAMETER GetEncryptionState  
59:    Returns the current encurrent state in an object as wolled :   
60:    
61:  .PARAMETER GetProtectionStatus  
62:    Returns the current protection status. It will return "Protected" if the drive is 100% encrypted, and "Unprotected" if anything else then "100% encrypted".  
63:    
64:  .PARAMETER Encrypt  
65:    Will encrypt a drive.  
66:    
67:  .PARAMETER TakeTPMOwnerShip  
68:    Returns true if allowed, false if not  
69:    
70:  .PARAMETER pin  
71:    Is needed in order to take the ownership and to encrypt the drive.  
72:    
73:  .PARAMETER IsTPMOwned  
74:    Returns true if owned, false if not  
75:    
76:  .PARAMETER GetKeyProtectorIds  
77:    Returns all the protector id's available on the machine.  
78:    
79:  .PARAMETER GetKeyProtectorType  
80:    Returns the type of protector that is currently in use.  
81:    
82:  .PARAMETER GetEncryptionMethod  
83:    REturns the current encryption method that is in use.  
84:    
85:  .PARAMETER GetKeyProtectorNumericalPassword  
86:    Returns a given numerical password based on a Protector ID value.  
87:    The ProtectorID value is mandatory, and must be passed using the parameter VolumeKeyProtectorID.   
88:    
89:  .PARAMETER VolumeKeyProtectorID  
90:    This parameter will work only in conjunction with GetKeyProtectorNumericalPassword switch.  
91:    It must contain the ProtectorID from which the desired Numerical Password will be returned.  
92:              
93:  .PARAMETER GetKeyProtectorTypeAndID  
94:    The GetKeyProtectorTypeAndID switch will return all the existing key protector ID's and their type of the Keys existing on the machine.  
95:    
96:    
97:  .PARAMETER Whatif  
98:       Permits to launch this script in "draft" mode. This means it will only show the results without really making generating the files.  
99:    
100:  .PARAMETER Verbose  
101:       Allow to run the script in verbose mode for debbuging purposes.  
102:      
103:  .EXAMPLE  
104:    
105:  BitLockerSAK  
106:    
107:  Returns the current status of the drives.  
108:    
109:  IsTPMOwned         : True  
110:  EncryptionMethod      : AES_128  
111:  IsTPMOwnerShipAllowed    : True  
112:  IsTPMActivated       : True  
113:  IsTPMEnabled        : True  
114:  CurrentEncryptionPercentage : 100  
115:  EncryptionState       : FullyEncrypted  
116:  ProtectorIds        : {{FFC19381-6E75-4D1E-94E9-D6E0D3E681FA}, {65AF5A93-9846-47AC-B3B1-D8DE6F06B780}}  
117:  KeyProtectorType      : {Numerical password, Trusted Platform Module (TPM)}  
118:    
119:  .EXAMPLE  
120:     
121:  BitLockerSAK -GetProtectionStatus  
122:    
123:  Returns the current protection status : Protected or unprotected  
124:    
125:  .EXAMPLE  
126:     
127:  BitLockerSAK -GetEncryptionState  
128:    
129:  CurrentEncryptionProgress is express in percentage.   
130:    
131:  CurrentEncryptionProgress EncryptionState                                             
132:  ------------------------- ---------------                                             
133:             100 FullyEncrypted  
134:      
135:  .EXAMPLE  
136:    
137:  Get all the key protectors and their respective ID's and protector types from the current machine.  
138:    
139:  BitLockerSAK -GetKeyProtectorTypeAndID  
140:    
141:  KeyProtectorID                                           KeyProtectorType                                            
142:  --------------                                           ----------------                                            
143:  {AB1535D4-ECB3-49D6-8AB1-E334A4F60579}                               Numerical password                                           
144:  {B1BDF8CD-55F2-4532-A93F-4B1AF4F22B55}                               Trusted Platform Module (TPM)  
145:    
146:  .EXAMPLE  
147:    
148:  Get the numerical password from a specefic Key using the ID.  
149:    
150:  BitLockerSAK -GetKeyProtectorNumericalPassword -VolumeKeyProtectorID "{AB1535D4-ECB3-49D6-8AB1-E334A4F60579}"  
151:    
152:  Message                              KeyProtectorNumericalPassword                   VolumeKeyProtectorID                         
153:  -------                              -----------------------------                   --------------------                         
154:  The method was successful.                     242968-693319-295251-477840-704451-214225-550055-383229      {AB1535D4-ECB3-49D6-8AB1-E334A4F60579}   
155:    
156:  .NOTES  
157:       -Author: Stephane van Gulick  
158:       -Email :   
159:       -CreationDate: 13-01-2014  
160:       -LastModifiedDate: 11.06.2015  
161:       -Version: 1.5  
162:       -History:  
163:    #0.1 : Created function  
164:    #1.1 : 20140901 Added GetProtectorIds  
165:    #1.2 : 20140909 Rewrote function  
166:            Added GetKeyprotectorType  
167:            Added EncryptionMethod  
168:       #1.3 : 20141003 Added TPM conditions.  
169:    #1.4 : Possiblity to select drive letter, added RemoveKeyProtectors.  
170:      --> GetKeyProtectorTypeAndID,  
171:      --> DeleteKeyProtectors,  
172:      --> ProtectorIDs,  
173:      --> DeleteKeyProtector,  
174:      --> PauseEncryption,  
175:      --> PauseDecryption,  
176:      --> Decrytp  
177:    #1.4.1 --> updated help with GetKeyPRotectedID instead of KeyProtectedID parameter  
178:    #1.5 Added GetKeyProtectorNumericalPassword and VolumeKeyProtectorID parameters.  
179:    
180:  .LINK  
181:    www.PowerShellDistrict.com  
182:    
183:  #>  
184:       [cmdletBinding()]  
185:       Param (  
186:            [Switch]$IsTPMActivated,  
187:            [Switch]$IsTPMEnabled,  
188:            [Switch]$IsTPMOwnerShipAllowed,  
189:            [Switch]$ResumeEncryption,  
190:            [Switch]$GetEncryptionState,  
191:            [Switch]$GetProtectionStatus,  
192:            [switch]$Encrypt,  
193:            [Parameter(ParameterSetName = 'OwnerShip')][switch]$TakeTPMOwnerShip,  
194:            [Parameter(ParameterSetName = 'OwnerShip')][int]$pin,  
195:            [switch]$IsTPMOwned,  
196:            [Switch]$GetKeyProtectorIds,  
197:            [switch]$GetEncryptionMethod,  
198:            [ValidateScript({  
199:                 if ($_ -match '^[A-Z]{1}[:]') {  
200:                      return $true  
201:                 } else {  
202:                      Write-Warning 'The drive letter parameter has to respect the following case: DriverLetter+Colomn EG: --> C: --> D: --> E: '  
203:                      return $false  
204:                 }  
205:            })][string]$DriveLetter = 'C:',  
206:            [switch]$GetKeyProtectorTypeAndID,  
207:            [switch]$DeleteKeyProtectors,  
208:            #Acceptvaluefrompipelinebyname  
209:            [String[]]$ProtectorIDs,  
210:            [switch]$DeleteKeyProtector,  
211:            [switch]$PauseEncryption,  
212:            [switch]$PauseDecryption,  
213:            [switch]$Decrytp,  
214:            [Parameter(ParameterSetName = 'NumericalPassword')][Switch]$GetKeyProtectorNumericalPassword,  
215:            [Parameter(ParameterSetName = 'NumericalPassword', Mandatory = $true)][String]$VolumeKeyProtectorID  
216:              
217:       )  
218:       Begin {  
219:            try {  
220:                 $Tpm = Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm -ErrorAction Stop  
221:            } catch [System.Management.ManagementException]{  
222:                   
223:                 write-warning 'Could not access the WMI methods. Verify that you run the script with elevated rights and try again.'  
224:                 continue  
225:                   
226:                   
227:            }  
228:       }  
229:       Process {  
230:            ##Add switch to verify if enough place is present on HD (6gig are need, 10 recommended).  
231:              
232:              
233:            switch ($PSBoundParameters.keys) {  
234:                   
235:                 'IsTPMActivated'{ $return = if ($Tpm) { $tpm.IsActivated().isactivated }; break }  
236:                 'IsTPMEnabled'{ $return = if ($Tpm) { $tpm.IsEnabled().isenabled }; break }  
237:                 'IsTPMOwnerShipAllowed'{ $return = if ($Tpm) { $tpm.IsOwnerShipAllowed().IsOwnerShipAllowed }; break }  
238:                 'IsTPMOwned'{ $return = if ($Tpm) { $Tpm.isowned().isowned }; break }  
239:                 'GetEncryptionState'{  
240:                      write-verbose "Getting the encryptionstate of drive $($driveletter)"  
241:                      #http://msdn.microsoft.com/en-us/library/aa376433(VS.85).aspx  
242:                      #We only want to work on the C: drive.  
243:                      $EncryptionData = Get-WmiObject -Namespace ROOT\CIMV2\Security\Microsoftvolumeencryption -Class Win32_encryptablevolume -Filter "DriveLetter = '$DriveLetter'"  
244:                      $protectionState = $EncryptionData.GetConversionStatus()  
245:                      $CurrentEncryptionProgress = $protectionState.EncryptionPercentage  
246:                        
247:                      switch ($ProtectionState.Conversionstatus) {  
248:                             
249:                           '0' {  
250:                                  
251:                                $Properties = @{ 'EncryptionState' = 'FullyDecrypted'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
252:                                $Return = New-Object psobject -Property $Properties  
253:                                  
254:                           }  
255:                             
256:                           '1' {  
257:                                  
258:                                $Properties = @{ 'EncryptionState' = 'FullyEncrypted'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
259:                                $Return = New-Object psobject -Property $Properties  
260:                                  
261:                           }  
262:                           '2' {  
263:                                  
264:                                $Properties = @{ 'EncryptionState' = 'EncryptionInProgress'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
265:                                $Return = New-Object psobject -Property $Properties  
266:                           }  
267:                           '3' {  
268:                                  
269:                                $Properties = @{ 'EncryptionState' = 'DecryptionInProgress'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
270:                                $Return = New-Object psobject -Property $Properties  
271:                           }  
272:                           '4' {  
273:                                  
274:                                $Properties = @{ 'EncryptionState' = 'EncryptionPaused'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
275:                                $Return = New-Object psobject -Property $Properties  
276:                           }  
277:                           '5' {  
278:                                  
279:                                $Properties = @{ 'EncryptionState' = 'DecryptionPaused'; 'CurrentEncryptionProgress' = $CurrentEncryptionProgress }  
280:                                $Return = New-Object psobject -Property $Properties  
281:                           }  
282:                           default {  
283:                                write-verbose "Couldn't retrieve an encryption state."  
284:                                $Properties = @{ 'EncryptionState' = $false; 'CurrentEncryptionProgress' = $false }  
285:                                $Return = New-Object psobject -Property $Properties  
286:                           }  
287:                      }  
288:                 }  
289:                 'ResumeEncryption'{  
290:                      write-verbose 'Resuming encryption'  
291:                      $ProtectionState = Get-WmiObject -Namespace ROOT\CIMV2\Security\Microsoftvolumeencryption -Class Win32_encryptablevolume -Filter "DriveLetter = '$DriveLetter'"  
292:                        
293:                      $Ret = $protectionState.ResumeConversion()  
294:                      $ReturnCode = $ret.ReturnValue  
295:                        
296:                      switch ($ReturnCode) {  
297:                             
298:                           ('0') { $Message = 'The Method Resume Conversion was called succesfully.' }  
299:                           ('2150694912') { $message = 'The volume is locked' }  
300:                           default { $message = 'The resume operation failed with an uknowned return code.' }  
301:                      }  
302:                        
303:                      $Properties = @{ 'ReturnCode' = $ReturnCode; 'ErrorMessage' = $message }  
304:                      $Return = New-Object psobject -Property $Properties  
305:                 } #EndResumeEncryption    
306:                 'GetProtectionStatus'{  
307:                      #http://msdn.microsoft.com/en-us/library/windows/desktop/aa376448(v=vs.85).aspx  
308:                      $ProtectionState = Get-WmiObject -Namespace ROOT\CIMV2\Security\Microsoftvolumeencryption -Class Win32_encryptablevolume -Filter "DriveLetter = '$DriveLetter'"  
309:                      write-verbose 'Gathering BitLocker protection status infos.'  
310:                        
311:                      switch ($ProtectionState.GetProtectionStatus().protectionStatus) {  
312:                             
313:                           ('0') { $return = 'Unprotected' }  
314:                           ('1') { $return = 'Protected' }  
315:                           ('2') { $return = 'Uknowned' }  
316:                           default { $return = 'NoReturn' }  
317:                      } #EndSwitch  
318:                 } #EndGetProtection  
319:                 'Encrypt'{  
320:                      #http://msdn.microsoft.com/en-us/library/windows/desktop/aa376432(v=vs.85).aspx  
321:                      $ProtectionState = Get-WmiObject -Namespace ROOT\CIMV2\Security\Microsoftvolumeencryption -Class Win32_encryptablevolume -Filter "DriveLetter = '$DriveLetter'"  
322:                      write-verbose 'Launching drive encryption.'  
323:                        
324:                      $ProtectorKey = $protectionState.ProtectKeyWithTPMAndPIN('ProtectKeyWithTPMAndPin', '', $pin)  
325:                      Start-Sleep -Seconds 3  
326:                      $NumericalPasswordReturn = $protectionState.ProtectKeyWithNumericalPassword()  
327:                        
328:                      $Return = $protectionState.Encrypt()  
329:                      $returnCode = $return.returnvalue  
330:                      switch ($ReturnCode) {  
331:                             
332:                           ('0') { $message = 'Operation successfully started.' }  
333:                           ('2147942487') { $message = 'The EncryptionMethod parameter is provided but is not within the known range or does not match the current Group Policy setting.' }  
334:                           ('2150694958') { $message = 'No encryption key exists for the volume' }  
335:                           ('2150694957') { $message = 'The provided encryption method does not match that of the partially or fully encrypted volume.' }  
336:                           ('2150694942') { $message = 'The volume cannot be encrypted because this computer is configured to be part of a server cluster.' }  
337:                           ('2150694956') { $message = 'No key protectors of the type Numerical Password are specified. The Group Policy requires a backup of recovery information to Active Directory Domain Services' }  
338:                           default {  
339:                                $message = 'An unknown status was returned by the Encryption action.'  
340:                                  
341:                           }  
342:                      }  
343:                        
344:                      $Properties = @{ 'ReturnCode' = $ReturnCode; 'ErrorMessage' = $message }  
345:                      $Return = New-Object psobject -Property $Properties  
346:                 }  
347:                 'GetKeyProtectorIds'{  
348:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
349:                      $return = $BitLocker.GetKeyProtectors('0').VolumeKeyProtectorID  
350:                 }  
351:                 'GetEncryptionMethod'{  
352:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
353:                      $EncryptMethod = $BitLocker.GetEncryptionMethod().encryptionmethod  
354:                      switch ($EncryptMethod) {  
355:                           '0'{ $Return = 'None'; break }  
356:                           '1'{ $Return = 'AES_128_WITH_DIFFUSER'; break }  
357:                           '2'{ $Return = 'AES_256_WITH_DIFFUSER'; break }  
358:                           '3'{ $Return = 'AES_128'; break }  
359:                           '4'{ $Return = 'AES_256'; break }  
360:                           '5'{ $Return = 'HARDWARE_ENCRYPTION'; break }  
361:                           default { $Return = 'UNKNOWN'; break }  
362:                      }  
363:                        
364:                 }  
365:                 'GetKeyProtectorTypeAndID'{  
366:                        
367:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
368:                      $ProtectorIds = $BitLocker.GetKeyProtectors('0').volumekeyprotectorID  
369:                        
370:                      $return = @()  
371:                        
372:                      foreach ($ProtectorID in $ProtectorIds) {  
373:                             
374:                           $KeyProtectorType = $BitLocker.GetKeyProtectorType($ProtectorID).KeyProtectorType  
375:                           $keyType = ''  
376:                           switch ($KeyProtectorType) {  
377:                                  
378:                                '0'{ $Keytype = 'Unknown or other protector type'; break }  
379:                                '1'{ $Keytype = 'Trusted Platform Module (TPM)'; break }  
380:                                '2'{ $Keytype = 'External key'; break }  
381:                                '3'{ $Keytype = 'Numerical password'; break }  
382:                                '4'{ $Keytype = 'TPM And PIN'; break }  
383:                                '5'{ $Keytype = 'TPM And Startup Key'; break }  
384:                                '6'{ $Keytype = 'TPM And PIN And Startup Key'; break }  
385:                                '7'{ $Keytype = 'Public Key'; break }  
386:                                '8'{ $Keytype = 'Passphrase'; break }  
387:                                '9'{ $Keytype = 'TPM Certificate'; break }  
388:                                '10'{ $Keytype = 'CryptoAPI Next Generation (CNG) Protector'; break }  
389:                                  
390:                           } #endSwitch  
391:                             
392:                           $Properties = @{ 'KeyProtectorID' = $ProtectorID; 'KeyProtectorType' = $Keytype }  
393:                           $Return += New-Object -TypeName psobject -Property $Properties  
394:                      } #EndForeach  
395:                        
396:                 } #EndGetKeyProtectorType  
397:                 'DeleteKeyProtectors'{  
398:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
399:                      $Return = $BitLocker.DeleteKeyProtectors()  
400:                        
401:                 }  
402:                 'TakeTPMOwnerShip'{  
403:                      $Tpm.takeOwnership()  
404:                 }  
405:                 'DeleteKeyProtector'{  
406:                        
407:                      if ($PSBoundParameters.ContainsKey('ProtectorIDs')) {  
408:                           $Return = @()  
409:                           $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
410:                             
411:                           foreach ($ProtID in $ProtectorIDs) {  
412:                                $Return += $BitLocker.DeleteKeyProtector($ProtID)  
413:                           }  
414:                      } else {  
415:                           write-warning 'Could not delete the key protector. Missing ProtectorID parameter.'  
416:                           $Return = 'Could not delete the key protector. Missing ProtectorID parameter.'  
417:                             
418:                      }  
419:                 }  
420:                 'PauseEncryption'{  
421:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
422:                      $ReturnCode = $BitLocker.PauseConversion()  
423:                        
424:                      switch ($ReturnCode.ReturnValue) {  
425:                           '0'{ $Return = 'Paused sucessfully.'; break }  
426:                           '2150694912'{ $Return = 'The volume is locked.'; Break }  
427:                           default { $Return = 'Uknown return code.'; break }  
428:                      }  
429:                 }  
430:                 'PauseDecryption'{  
431:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
432:                      $ReturnCode = $BitLocker.PauseConversion()  
433:                        
434:                      switch ($ReturnCode.ReturnValue) {  
435:                           '0'{ $Return = 'Paused sucessfully.'; break }  
436:                           '2150694912'{ $Return = 'The volume is locked.'; Break }  
437:                           default { $Return = 'Uknown return code.'; break }  
438:                      }  
439:                 }  
440:                 'Decrytp'{  
441:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
442:                      $ReturnCode = $BitLocker.Decrypt()  
443:                        
444:                      switch ($ReturnCode.ReturnValue) {  
445:                           '0'{ $Return = 'Uncryption started successfully.'; break }  
446:                           '2150694912'{ $Return = 'The volume is locked.'; Break }  
447:                           '2150694953' { $Return = 'This volume cannot be decrypted because keys used to automatically unlock data volumes are available.'; Break }  
448:                           default { $Return = 'Uknown return code.'; break }  
449:                      }  
450:                        
451:                 }  
452:                 'GetKeyProtectorNumericalPassword'{  
453:                      $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
454:                      $Return = @()  
455:                        
456:                        
457:                      $KeyProtectorReturn = $BitLocker.GetKeyProtectorNumericalPassword($VolumeKeyProtectorID)  
458:                        
459:                      switch ($KeyProtectorReturn.ReturnValue) {  
460:                           '0' { $msg = 'The method was successful.' }  
461:                           '2150694912' { $msg = 'The volume is locked.'; Break }  
462:                           '2147942487' { $msg = "The VolumeKeyProtectorID parameter does not refer to a key protector of the type 'Numerical Password'."; Break }  
463:                           '2150694920' { $msg = 'BitLocker is not enabled on the volume. Add a key protector to enable BitLocker.'; Break }  
464:                           default { $msg = "Unknown return value: $($KeyProtectorReturn.ReturnValue)" }  
465:                      } #EndSwitch  
466:                        
467:                      $Properties = @{ 'KeyProtectorNumericalPassword' = $KeyProtectorReturn.NumericalPassword; 'VolumeKeyProtectorID' = $VolumeKeyProtectorID; 'Message' = $msg }  
468:                      $Return += New-Object -TypeName psobject -Property $Properties  
469:                        
470:                        
471:                 }  
472:            } #endSwitch  
473:              
474:              
475:            if ($PSBoundParameters.Keys.Count -eq 0) {  
476:                 #Returning info on all drives.  
477:                 write-verbose 'Returning bitlocker main status'  
478:                 $Tpm = Get-WmiObject -Namespace ROOT\CIMV2\Security\MicrosoftTpm -Class Win32_Tpm  
479:                 $BitLocker = Get-WmiObject -Namespace 'Root\cimv2\Security\MicrosoftVolumeEncryption' -Class 'Win32_EncryptableVolume' -Filter "DriveLetter = '$DriveLetter'"  
480:                   
481:                 #If no TPM module is present  
482:                 if ($tpm) {  
483:                      $TpmActivated = $tpm.IsActivated().isactivated  
484:                      $TPMEnabled = $tpm.IsEnabled().isenabled  
485:                      $TPMOwnerShipAllowed = $Tpm.IsOwnershipAllowed().IsOwnerShipAllowed  
486:                      $TPMOwned = $Tpm.isowned().isowned  
487:                        
488:                 }  
489:                   
490:                 $ProtectorIds = $BitLocker.GetKeyProtectors('0').volumekeyprotectorID  
491:                 $CurrentEncryptionState = BitLockerSAK -GetEncryptionState  
492:                 $EncryptionMethod = BitLockerSAK -GetEncryptionMethod  
493:                 $KeyProtectorTypeAndID = BitLockerSAK -GetKeyProtectorTypeAndID  
494:                   
495:                 $properties = @{  
496:                      'IsTPMActivated' = $TpmActivated;`  
497:                      'IsTPMEnabled' = $TPMEnabled;`  
498:                      'IsTPMOwnerShipAllowed' = $TPMOwnerShipAllowed;`  
499:                      'IsTPMOwned' = $TPMOwned;`  
500:                      'CurrentEncryptionPercentage' = $CurrentEncryptionState.CurrentEncryptionProgress;`  
501:                      'EncryptionState' = $CurrentEncryptionState.encryptionState; `  
502:                      'EncryptionMethod' = $EncryptionMethod;`  
503:                      'KeyProtectorTypesAndIDs' = $KeyProtectorTypeAndID  
504:                 }  
505:                   
506:                 $Return = New-Object psobject -Property $Properties  
507:            }  
508:              
509:       }  
510:       End {  
511:            return $return  
512:       }  
513:         
514:  }  
515:    
516:  function Get-BiosStatus {  
517:       param ([String]$Option)  
518:         
519:       #Declare Local Variables  
520:       Set-Variable -Name Argument -Scope Local -Force  
521:       Set-Variable -Name CCTK -Value "x:\cctk\cctk.exe" -Scope Local -Force  
522:       Set-Variable -Name Output -Scope Local -Force  
523:         
524:       $Argument = "--" + $Option  
525:       $Output = [string] (& $CCTK $Argument)  
526:       $Output = $Output.Split('=')  
527:       Return $Output[1]  
528:         
529:       #Cleanup Local Variables  
530:       Remove-Variable -Name Argument -Scope Local -Force  
531:       Remove-Variable -Name CCTK -Scope Local -Force  
532:       Remove-Variable -Name Output -Scope Local -Force  
533:  }  
534:    
535:  #Declare Local Variables  
536:  Set-Variable -Name oShell -Scope Local -Force  
537:  Set-Variable -Name TPMActivated -Scope Local -Force  
538:  Set-Variable -Name TPMEnabled -Scope Local -Force  
539:  Set-Variable -Name TPMOwned -Scope Local -Force  
540:  Set-Variable -Name TPMOwnershipAllowed -Scope Local -Force  
541:    
542:  cls  
543:    
544:  $TPMEnabled = Get-BiosStatus -Option "tpm"  
545:  Write-Host "TPM Enabled:"$TPMEnabled  
546:  $TPMActivated = Get-BiosStatus -Option "tpmactivation"  
547:  Write-Host "TPM Activated:"$TPMActivated  
548:  $TPMOwnershipAllowed = BitLockerSAK -IsTPMOwnerShipAllowed  
549:  Write-Host "TPM Ownership Allowed:"$TPMOwnershipAllowed  
550:  $TPMOwned = BitLockerSAK -IsTPMOwned  
551:  Write-Host "TPM Owned:"$TPMOwned  
552:  Write-Host  
553:  If (($TPMEnabled -eq "on") -and ($TPMActivated -eq "activate") -and ($TPMOwnershipAllowed -eq $true) -and ($TPMOwned -eq $false)) {  
554:       Write-Host "TPM Ready for Bitlocker"  
555:       Exit 0  
556:  } else {  
557:       Write-Host "TPM Not Ready for Bitlocker"  
558:       $oShell = New-Object -ComObject Wscript.Shell  
559:       $oShell.Popup("TPM Not Ready for Bitlocker." + [char]13 + [char]13 + "Turn On, Clear, and Activate TPM, then restart image.", 0, "TPM Failure", 0x0)  
560:       Exit 1  
561:  }  
562:    
563:  #Cleanup Local Variables  
564:  Remove-Variable -Name oShell -Scope Local -Force  
565:  Remove-Variable -Name TPMActivated -Scope Local -Force  
566:  Remove-Variable -Name TPMEnabled -Scope Local -Force  
567:  Remove-Variable -Name TPMOwned -Scope Local -Force  
568:  Remove-Variable -Name TPMOwnershipAllowed -Scope Local -Force  
569:    

25 January 2016

Fix for A connection to the deployment could not be made

Posted By: Mick Pletcher - 3:59 PM
This error is very annoying when running through a build. I Googled and found this fix to it by Kyle Beckman. The trick is delaying the LiteTouch.wsf from trying to execute the next task before network connectivity is available. Kyle's fix is putting a simple timed delay in the LiteTouch.wsf. I decided to enhance this fix by using PowerShell. Instead of having to guess at how long to wait, why not use the Test-Connection commandlet and delay the script until connectivity is available? That is what I did and it has worked perfectly. The first thing was to create the oShell object. Next use oShell.run to execute the powershell command. I used the -command so that it did not have to execute and external script. You will also need to customize the part that says <IP or DNS Address>. I use the fully qualified domain name of our DNS server to test for network connectivity. You can see the code I added in the field below:


           Set oShell = CreateObject("Wscript.shell")  
           oShell.run("powershell.exe -executionpolicy bypass -command Do {$Connectivity = Test-Connection -Destination <IP or DNS Address> -Count 1 -Quiet} while ($Connectivity -eq $false)")  
   

This is what the code looks like as a whole within the LiteTouch.wsf file:

      Function ValidateDeployRootWithRecovery  
   
           Dim sARF  
           Dim sNetworkErrorHint  
           DIm iRetVal  
           Dim sLTISuspend  
   
           'Wait for network connectivity  
           Set oShell = CreateObject("Wscript.shell")  
           oShell.run("powershell.exe -executionpolicy bypass -command Do {$Connectivity = Test-Connection -Destination <IP or DNS Address> -Count 1 -Quiet} while ($Connectivity -eq $false)")  
           If oUtility.ValidateConnectionEx(oEnvironment.Item("DeployRoot"), True) = Success then  
                ValidateDeployRootWithRecovery = SUCCESS  
                exit function  
           End if  
   


NOTE: This will require at least PowerShell version 3. Also, I would highly recommend making a copy of LiteTouch.wsf before making any modifications to it. 

20 January 2016

Configuring Dell BIOS Before the Image is Laid Down

Posted By: Mick Pletcher - 4:19 PM
In 2014, I published this blog entry that configured the BIOS during a build process. It used the Dell Command | Configure utility. At the time, I was in a squeeze to get the new build created along with getting all new GPOs and upgrading to a new document management system at the same time. 

I now have had some time to dig in and finally solved through Henk Hoogendoorn's blog on how to set the BIOS settings pre-image. To use this in MDT/SCCM before the image is laid down, you will need to do the following:
  1. Download the Dell Command | Configure from here.
  2. Install the package on your machine.
  3. Go to the %PROGRAMFILES%\Dell\Command Configure\X86 directory. Use the X86_64 if it is a 64-bit WinPE.
  4. Copy the contents of the folder, including the HAPI subfolder to a network share
  5. Go into the task sequence and add a Group named Configure BIOS
  6. I then created a subgroup named Install CCTK to install and run the CCTK during the build process
  7. Under Install CCTK, create a Run Command Line to map a drive letter to the network location that contains the CCTK files you copied over earlier. You will need a specific drive letter to address in the next step. I used the letter t:. The command line I used was net use t: \\Production Applications\Dell\CCTK  /USER:<DOMAIN>\<Username> Password
  8. Create a second Run Command Line that will copy over all of the files from the network share to the X: drive. I named it Install CCTK. The command line I used is:  xcopy.exe "t:\*.*" "x:\CCTK\" /E /C /I /H /R /Y /V
  9. Create a third Run Command Line that will Enable the HAPI. The entry I used was: x:\CCTK\HAPI\hapint -i -k C-C-T-K -p X:\CCTK\HAPI\.
It is now ready to execute the individual CCTK.exe commands to set BIOS settings. You will create an individual command line task entry for each setting you want to change in the BIOS. One example I set is to disable the chassis intrusion. This is what the command line looks like: x:\CCTK\CCTK.exe --chasintrusion=disable. The next thing you need to do for each entry is to add 119 and 240 to the success codes. My success code for each BIOS setting looks like this: 0 119 240 3010. Here is a screenshot of what my task sequence for setting the BIOS looks like. As you can see, I have a subgroup for Install CCTK, All Systems, Desktop, Laptop, and Enable Bitlocker. There are specific BIOS settings for desktops and laptops. I include a task sequence variable IsDesktop equals TRUE for the Desktop folder and IsLaptop equal TRUE for the Laptop group. The enable Bitlocker is also limited by a series of Task sequence variables I use.


The difference in time of setting these settings pre-image and setting them post-image is like night and day. On a 9010, it sets all BIOS settings within 15 seconds, whereas it took more than a minute post-image. 


18 January 2016

Installing Microsoft Updates in the Build Post-OS

Posted By: Mick Pletcher - 5:51 PM









There are updates that have to be installed after the OS has been installed. .Net Framework is an example. When I am building a reference image, I cannot inject the updates as packages to .Net Framework as it is not installed until after the OS has been installed. There have also been cases where an MSU could not be injected before the OS was installed. It would kill the build. I don't want to waste time having to wait on the windows update task to download, verify, and install the update.  This script will solve these issues. The script is designed to get all EXE and MSU files in the same directory as the InstallOnlineUpdates.ps1 PowerShell script and install them.

The first thing you want to do is let the reference image complete. Once it is finished, look at the ZTIWindowsUpdate.log file. It will show all updates that were downloaded and installed during the Windows Update task. The next thing is to download the updates, which can easily be done at https://catalog.update.microsoft.com/. Once you have the updates downloaded, place them in the same directory as this PowerShell script. In order to sequence these out, number them starting from 01 and etc. You can see how I have done mine in the screenshot below. I put EXE and MSU in the front of the filename so they would be separated and easy to find. 

Now all you have to do is to create an application to execute the PowerShell script and it will install all of your EXE and MSU files that need to be installed post-OS. 

You can download the script from here.


1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
5:        Created on:       1/18/2016 1:41 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        InstallOnlineUpdates.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will install both exe updates and msu updates. It is  
12:            intended to be used during the generation of a reference image. Not  
13:            all updates can be injected offline into the OS. This script takes  
14:            care of those by running post-OS install.  
15:  #>  
16:    
17:    
18:  function Install-EXEUpdates {  
19:       #Declare Variables  
20:       Set-Variable -Name Arguments -Scope Local -Force  
21:       Set-Variable -Name ErrCode -Scope Local -Force  
22:       Set-Variable -Name File -Scope Local -Force  
23:       Set-Variable -Name EXEFiles -Scope Local -Force  
24:       Set-Variable -Name RelativePath -Scope Local -Force  
25:         
26:       $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
27:       $EXEFiles = Get-ChildItem -Path $RelativePath -Force | where { $_.Name -like "*.exe*" }  
28:       If ($EXEFiles.Count -ge 1) {  
29:            $EXEFiles | Sort-Object  
30:            cls  
31:            foreach ($File in $EXEFiles) {  
32:                 $Arguments = "/passive /norestart"  
33:                 Write-Host "Installing"$File.Name"....." -NoNewline  
34:                 $ErrCode = (Start-Process -FilePath $File.Fullname -ArgumentList $Arguments -Wait -Passthru).ExitCode  
35:                 If ($ErrCode -eq 0) {  
36:                      Write-Host "Success" -ForegroundColor Yellow  
37:                 } else {  
38:                      Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
39:                 }  
40:            }  
41:       }  
42:    
43:       #Cleanup Local Variables  
44:       Remove-Variable -Name Arguments -Scope Local -Force  
45:       Remove-Variable -Name ErrCode -Scope Local -Force  
46:       Remove-Variable -Name File -Scope Local -Force  
47:       Remove-Variable -Name EXEFiles -Scope Local -Force  
48:       Remove-Variable -Name RelativePath -Scope Local -Force  
49:  }  
50:    
51:  function Install-MSUUpdates {  
52:       #Declare Variables  
53:       Set-Variable -Name Arguments -Scope Local -Force  
54:       Set-Variable -Name ErrCode -Scope Local -Force  
55:       Set-Variable -Name Executable -Value $env:windir"\System32\wusa.exe" -Scope Local -Force  
56:       Set-Variable -Name File -Scope Local -Force  
57:       Set-Variable -Name MSUFiles -Scope Local -Force  
58:       Set-Variable -Name RelativePath -Scope Local -Force  
59:         
60:       $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
61:       $MSUFiles = Get-ChildItem -Path $RelativePath -Force | where { $_.Name -like "*.msu*" }  
62:       If ($MSUFiles.Count -ge 1) {  
63:            $MSUFiles | Sort-Object  
64:            cls  
65:            foreach ($File in $MSUFiles) {  
66:                 $Arguments = $File.FullName + [char]32 + "/quiet /norestart"  
67:                 Write-Host "Installing"$File.Name"....." -NoNewline  
68:                 $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Arguments -Wait -Passthru).ExitCode  
69:                 If (($ErrCode -eq 0) -or ($ErrCode -eq 2359302)) {  
70:                      Write-Host "Success" -ForegroundColor Yellow  
71:                 } else {  
72:                      Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
73:                 }  
74:            }  
75:       }  
76:         
77:       #Cleanup Local Variables  
78:       Remove-Variable -Name Arguments -Scope Local -Force  
79:       Remove-Variable -Name ErrCode -Scope Local -Force  
80:       Remove-Variable -Name Executable -Scope Local -Force  
81:       Remove-Variable -Name File -Scope Local -Force  
82:       Remove-Variable -Name MSUFiles -Scope Local -Force  
83:       Remove-Variable -Name RelativePath -Scope Local -Force  
84:  }  
85:    
86:  cls  
87:  Install-EXEUpdates  
88:  Install-MSUUpdates  
89:  Start-Sleep -Seconds 5  
90:    

29 December 2015

Microsoft Office Updater

Posted By: Mick Pletcher - 4:00 PM









One of the things that has annoyed me in the past is having to keep the Microsoft Office Updates folder up-to-date with the latest .MSP files. The reason I bother with keeping the Updates folder up-to-date is because it speeds up the process of building a golden image, which in my environment, Office is installed during that phase. To automate this process, I created an automatic deployment rule in SCCM that keeps the Microsoft Office software update group up-to-date with the latest Office updates. To bridge those updates with the Updates folder under the Office installation directory, I wrote the script below.

The script is setup to run as a scheduled task on the SCCM server once a week. It does not have to be setup to execute on a schedule, but it does make for one less thing to keep up with. The variables you will have to modify are $Country, $Source, and $Destination. You specify which country you need to include in the $Country variable on line 29. If you need multiple countries, you will have to modify this script. $Source will be the directory where the Deployment Package is written to on line 30. $Destination is the location of the Microsoft Office Updates folder on line 31. The files located on the SCCM server are all .CAB files, so this script automatically extracts the .MSP files from the .CAB files and places them in the Updates directory.

You can download the script from here.



1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.99  
5:        Created on:       12/29/2015 9:50 AM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        OfficeUpdater.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will keep the Updates folder populated with the latest Office  
12:            updates that SCCM has downloaded. It should be setup to execute as a   
13:            scheduled task on the SCCM server. I suggest executing it once a week.   
14:  #>  
15:    
16:  #Declare Variables  
17:  Set-Variable -Name Counter -Value 0 -Scope Local -Force  
18:  Set-Variable -Name Country -Scope Local -Force  
19:  Set-Variable -Name Destination -Scope Local -Force  
20:  Set-Variable -Name Executable -Scope Local -Force  
21:  Set-Variable -Name File -Scope Local -Force  
22:  Set-Variable -Name Files -Scope Local -Force  
23:  Set-Variable -Name Folder -Scope Local -Force  
24:  Set-Variable -Name Folders -Scope Local -Force  
25:  Set-Variable -Name Output -Scope Local -Force  
26:  Set-Variable -Name Parameters -Scope Local -Force  
27:  Set-Variable -Name Source -Scope Local -Force  
28:    
29:  $Country = "en-us"  
30:  $Source = "<SCCM Updates Source Directory>"  
31:  $Destination = "<Microsoft Office Updates folder>"  
32:  $Executable = $env:windir + "\System32\expand.exe"  
33:  If ($Country -notcontains "`*") {  
34:       $Country = "*" + $Country + "*"  
35:  }  
36:  $Folders = Get-ChildItem -Path $Source -Recurse | ?{ $_.PSIsContainer }  
37:  foreach ($Folder in $Folders) {  
38:       cls  
39:       $Counter++  
40:       $Output = "Processing " + $Counter + " of " + $Folders.Count  
41:       Write-Host $Output  
42:       $Files = Get-ChildItem -Path $Folder.Fullname -Recurse  
43:       foreach ($File in $Files) {  
44:            If (($File.Name -like $Country) -or ($File.Name -like "*none*")) {  
45:                 $Parameters = [char]34 + $File.FullName + [char]34 + [char]32 + [char]34 + $Destination + [char]34 + [char]32 + "-f:*"  
46:                 $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
47:                 #Copy-Item -Path $File.FullName -Destination $Destination -Force  
48:            }  
49:       }  
50:  }  
51:    
52:  #Cleanup Variables  
53:  Remove-Variable -Name Counter -Scope Local -Force  
54:  Remove-Variable -Name Country -Scope Local -Force  
55:  Remove-Variable -Name Destination -Scope Local -Force  
56:  Remove-Variable -Name Executable -Scope Local -Force  
57:  Remove-Variable -Name File -Scope Local -Force  
58:  Remove-Variable -Name Files -Scope Local -Force  
59:  Remove-Variable -Name Folder -Scope Local -Force  
60:  Remove-Variable -Name Folders -Scope Local -Force  
61:  Remove-Variable -Name Output -Scope Local -Force  
62:  Remove-Variable -Name Parameters -Scope Local -Force  
63:  Remove-Variable -Name Source -Scope Local -Force  
64:    

28 December 2015

Automating the Creation of Software Update Groups in SCCM

Posted By: Mick Pletcher - 2:58 PM














I have been working on automating the tasks of deploying Windows updates each month. You may think why is there a need for this when SCCM has the Automatic Deployment Rules. Some companies have to review the updates before hand if they have applications that are vulnerable to breaking from windows updates.

This script is the first of a series of steps to achieve this task. This script will automatically create a Software Update Group in SCCM. To further assist the admins, I have also included the ability for the script to generate an excel spreadsheet with a list of the updates that were added to the newly created Update Group. The script then emails the spreadsheet to the designated admin(s). This allows for two things: 1) It reminds the admin that it's that time of the month again, and 2) It allows the admin to review the updates before downloading them, if there are any that need to be removed. That is why I am not automating the package creation portion.

There are three variables that need to be populated: $EmailAddresses, $OperatingSystem, and $Architecture. You can enter multiple email addresses in the $EmailAddresses array. The two optional variables are $UpdateTypes and $Filters. Do not change $Updates.

The script has the ability to specify updates using three different criteria. Set-TimeSpanByMonthsOld lets you specify to include all updates say one month old. My firm deploys updates one month old so that if there are issues in newly released ones, they will typically have been resolved after a month. Say it is currently December, this function will specify all updates from 11/1 to 11/30.

The second criteria is Set-TimeSpanByDatePeriod. This allows for you to specify a specific date range if you are doing a one-off deployment.

The third is Set-TimeSpanAllUpdatesBeforeDate. This allows for you to specify all updates before a specific date to be included.

This script has to be executed on the SCCM server. I have set the script up as a scheduled task that is executed the second Wednesday of every month. You will need to change line 19 to the location of the SCCM module on your server.

I have commented out a few examples within the script.

You can download the script from here.




1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       12/28/2015 9:08 AM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        SoftwareUpdateGroupCreator.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will create a new software update group each month. It then  
12:            generates an email to send to the admin(s) to review what updates  
13:            were put into the new update group. It does not download the updates so  
14:            that if an update needs to be removed, it can be done before it is   
15:            downloaded.   
16:              
17:  #>  
18:    
19:  Import-Module "D:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"  
20:    
21:  function Get-Updates {  
22:       param ([String]$Architecture, [String]$OperatingSystem, [String]$Namespace, $Updates )  
23:         
24:       $Updates = @()  
25:       $Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace  
26:       $Updates = $Updates | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) }  
27:       If ($Architecture -eq "x86") {  
28:            $Updates = $Updates | where-object { ($_.LocalizedDisplayName -notmatch "x64") }  
29:       } elseif ($Architecture -eq "x64") {  
30:            #$Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) -and ($_.LocalizedDisplayName -match "x64-based Systems") }  
31:            $Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) -and ($_.LocalizedDisplayName -match "x64") }  
32:       }  
33:       Return $Updates  
34:  }  
35:    
36:  function Set-Filters {  
37:       param($Filters, $Updates, $UpdateTypes)  
38:         
39:       #Declare Variables  
40:       Set-Variable -Name Filter -Scope Local -Force  
41:       Set-Variable -Name UpdateType -Scope Local -Force  
42:         
43:       foreach ($Filter in $Filters) {  
44:            $Updates = $Updates | Where-Object { $_.LocalizedDisplayName -notmatch $Filter }  
45:       }  
46:       If ($UpdateTypes.Count -ge 1) {  
47:            foreach ($UpdateType in $UpdateTypes) {  
48:                 $Updates = $Updates | Where-Object { $_.LocalizedCategoryInstanceNames -match $UpdateType }  
49:            }  
50:            If ($UpdateTypes -eq "Update") {  
51:                 $Updates = $Updates | Where-Object { $_.LocalizedDisplayName -notmatch "Security Update" }  
52:            }  
53:       }  
54:       return $Updates  
55:         
56:       #Cleanup Variables  
57:       Remove-Variable -Name Filter -Scope Local -Force  
58:       Remove-Variable -Name UpdateType -Scope Local -Force  
59:  }  
60:    
61:  function Set-TimeSpanByMonthsOld {  
62:       param ($MonthsOld, $Updates)  
63:         
64:       #Declare Local Variables  
65:       Set-Variable -Name Day -Scope Local -Force  
66:       Set-Variable -Name Month -Scope Local -Force  
67:       Set-Variable -Name FirstDayOfMonth -Scope Local -Force  
68:       Set-Variable -Name LastDayOfMonth -Scope Local -Force  
69:       Set-Variable -Name Today -Scope Local -Force  
70:         
71:       If ($MonthsOld -ge 1) {  
72:            $MonthsOld = $MonthsOld * -1  
73:       }  
74:       $Today = Get-Date  
75:       $Month = $Today.AddMonths($MonthsOld)  
76:       $Day = $Month.Day  
77:       $FirstDayOfMonth = $Month.AddDays(($Day - 1) * -1)  
78:       $LastDayOfMonth = [System.DateTime]::DaysInMonth($Month.Year, $Month.Month)  
79:       $LastDayOfMonth = $LastDayOfMonth - 1  
80:       $LastDayOfMonth = $FirstDayOfMonth.AddDays($LastDayOfMonth)  
81:       $FirstDayOfMonth = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($FirstDayOfMonth)  
82:       $LastDayOfMonth = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($LastDayOfMonth)  
83:       $Updates = $Updates | Where { ($_.DateCreated -ge $FirstDayOfMonth) -and ($_.DateCreated -le $LastDayOfMonth) }  
84:       return $Updates  
85:         
86:       #Cleanup Local Variables  
87:       Remove-Variable -Name Day -Scope Local -Force  
88:       Remove-Variable -Name Month -Scope Local -Force  
89:       Remove-Variable -Name FirstDayOfMonth -Scope Local -Force  
90:       Remove-Variable -Name LastDayOfMonth -Scope Local -Force  
91:  }  
92:    
93:  function Set-TimeSpanByDatePeriod {  
94:       param ([String]$StartDate,  
95:            [String]$EndDate,  
96:            $Updates)  
97:         
98:       $StartDate = [DateTime]$StartDate  
99:       $StartDate = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($StartDate)  
100:       $EndDate = [DateTime]$EndDate  
101:       $EndDate = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($EndDate)  
102:       $Updates = $Updates | Where { ($_.DateCreated -ge $StartDate) -and ($_.DateCreated -le $EndDate) }  
103:       return $Updates  
104:  }  
105:    
106:  function Set-TimeSpanAllUpdatesBeforeDate {  
107:       param ([String]$StartDate, $Updates)  
108:         
109:       $StartDate = [DateTime]$StartDate  
110:       $StartDate = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($StartDate)  
111:       $Updates = $Updates | Where { $_.DateCreated -lt $StartDate }  
112:       return $Updates  
113:  }  
114:    
115:  function CreateSoftwareUpdateGroup {  
116:       param ($OperatingSystem, $Architecture, $Updates)  
117:         
118:       #Declare Variables  
119:       Set-Variable -Name Description -Scope Local -Force  
120:       Set-Variable -Name Month -Scope Local -Force  
121:       Set-Variable -Name SoftwareUpdateGroupName -Scope Local -Force  
122:       Set-Variable -Name SoftwareUpdates -Scope Local -Force  
123:       Set-Variable -Name Temp -Scope Local -Force  
124:       Set-Variable -Name Update -Scope Local -Force  
125:       Set-Variable -Name Year -Scope Local -Force  
126:         
127:       $SoftwareUpdates = @()  
128:       $Year = (Get-Date).Year  
129:       $Month = Get-Date -format "MMMM"  
130:       $SoftwareUpdateGroupName = $OperatingSystem + $Architecture + [char]32 + $Month + [char]45 + $Year  
131:       $Description = $SoftwareUpdateGroupName + [char]32 + "Updates"  
132:       foreach ($Update in $Updates) {  
133:            $SoftwareUpdates += ($Update.CI_ID)  
134:       }  
135:       cd BNA:  
136:       $TEMP = New-CMSoftwareUpdateGroup -Name $SoftwareUpdateGroupName -UpdateID $SoftwareUpdates -Description $Description  
137:       cd c:  
138:       $SoftwareUpdates = $null  
139:         
140:       #Cleanup Variables  
141:       Remove-Variable -Name Description -Scope Local -Force  
142:       Remove-Variable -Name Month -Scope Local -Force  
143:       Remove-Variable -Name SoftwareUpdateGroupName -Scope Local -Force  
144:       Remove-Variable -Name SoftwareUpdates -Scope Local -Force  
145:       Remove-Variable -Name Temp -Scope Local -Force  
146:       Remove-Variable -Name Update -Scope Local -Force  
147:       Remove-Variable -Name Year -Scope Local -Force  
148:  }  
149:    
150:  function ProcessLogFile {  
151:       param([String]$OperatingSystem, [String]$Architecture)  
152:         
153:       #Declare Local Variables  
154:       Set-Variable -Name LogFile -Scope Local -Force  
155:       Set-Variable -Name Month -Scope Local -Force  
156:       Set-Variable -Name Output -Scope Local -Force  
157:       Set-Variable -Name temp -Scope Local -Force  
158:         
159:       $Month = Get-Date -format "MMMM"  
160:       $OperatingSystem = $OperatingSystem -replace '\s',''  
161:       $LogFile = $env:TEMP + "\" + $OperatingSystem + $Architecture + $Month + "UpdatesReport.csv"  
162:       if ((Test-Path $LogFile) -eq $true) {  
163:            Remove-Item $LogFile -Force  
164:       }  
165:       if ((Test-Path $LogFile) -eq $false) {  
166:            $temp = New-Item $LogFile -ItemType file -Force  
167:            $Output = "Update Name, Article ID, Update Type, Release Date"  
168:            Out-File -FilePath $LogFile -InputObject $Output -Force -Encoding UTF8  
169:       }  
170:       Return $LogFile  
171:         
172:       #Cleanup Local Variables  
173:       Remove-Variable -Name LogFile -Scope Local -Force  
174:       Remove-Variable -Name Month -Scope Local -Force  
175:       Remove-Variable -Name Output -Scope Local -Force  
176:       Remove-Variable -Name temp -Scope Local -Force  
177:  }  
178:    
179:  function New-Report {  
180:       param($EmailAddressList, $Updates, $OperatingSystem, $Architecture)  
181:         
182:       #Declare Variables  
183:       Set-Variable -Name ArticleID -Scope Local -Force  
184:       Set-Variable -Name Body -Scope Local -Force  
185:       Set-Variable -Name DateCreated -Scope Local -Force  
186:       Set-Variable -Name EmailAddress -Scope Local -Force  
187:       Set-Variable -Name Month -Scope Local -Force  
188:       Set-Variable -Name Output -Scope Local -Force  
189:       Set-Variable -Name Subject -Scope Local -Force  
190:       Set-Variable -Name Update -Scope Local -Force  
191:         
192:       foreach ($Update in $Updates) {  
193:            $Update.LocalizedDisplayName = $Update.LocalizedDisplayName -replace ",", ""  
194:            $ArticleID = "KB" + $Update.ArticleID  
195:            [String]$DateCreated = [System.Management.ManagementDateTimeConverter]::ToDateTime($Update.DateCreated)  
196:            If ($Update.LocalizedCategoryInstanceNames -match "Security Updates") {  
197:                 $Output = $Update.LocalizedDisplayName + "," + $ArticleID + ",Security Update," + $DateCreated  
198:            } elseif (($Update.LocalizedCategoryInstanceNames -notmatch "Security Updates") -and ($Update.LocalizedCategoryInstanceNames -match "Update")) {  
199:                 $Output = $Update.LocalizedDisplayName + "," + $ArticleID + ",Update," + $DateCreated  
200:            } else {  
201:                 $Output = $Update.LocalizedDisplayName + "," + $ArticleID + ", ," + $DateCreated  
202:            }  
203:            Out-File -FilePath $LogFile -InputObject $Output -Append -Force -Encoding UTF8  
204:       }  
205:       $Month = Get-Date -format "MMMM"  
206:       $Subject = $OperatingSystem + $Architecture + [char]32 + $Month + [char]32 + "SCCM Windows Update List"  
207:       $Body = "List of Windows updates added to the" + $OperatingSystem + $Architecture + [char]32 + $Month + " software update group."  
208:       foreach ($EmailAddress in $EmailAddressList) {  
209:            Send-MailMessage -To $EmailAddress -From "engineers@wallerlaw.com" -Subject $Subject -Body $Body -Attachments $LogFile -SmtpServer "smtp.wallerlaw.com"  
210:       }  
211:       $EmailAddresses = $null  
212:         
213:       #Cleanup Variables  
214:       Remove-Variable -Name ArticleID -Scope Local -Force  
215:       Remove-Variable -Name Body -Scope Local -Force  
216:       Remove-Variable -Name DateCreated -Scope Local -Force  
217:       Remove-Variable -Name EmailAddress -Scope Local -Force  
218:       Remove-Variable -Name Month -Scope Local -Force  
219:       Remove-Variable -Name Output -Scope Local -Force  
220:       Remove-Variable -Name Subject -Scope Local -Force  
221:       Remove-Variable -Name Update -Scope Local -Force  
222:  }  
223:    
224:  #Declare Variables  
225:  Set-Variable -Name Architecture -Scope Local -Force  
226:  Set-Variable -Name EmailAddresses -Scope Local -Force  
227:  Set-Variable -Name Filters -Scope Local -Force  
228:  Set-Variable -Name LogFile -Scope Local -Force  
229:  Set-Variable -Name Namespace -Value root\sms\site_bna -Scope Local -Force  
230:  Set-Variable -Name OperatingSystem -Scope Local -Force  
231:  Set-Variable -Name Updates -Scope Local -Force  
232:  Set-Variable -Name UpdateTypes -Scope Local -Force  
233:    
234:  cls  
235:  $EmailAddresses = @("mick.pletcher@test.com")  
236:  $OperatingSystem = "Windows 7" #Windows 7, Windows 8.1, Windows Server 2012 R2  
237:  $Architecture = "x86" #x86, x64, or null  
238:  $UpdateTypes = @() #Security Update, Update, Service Pack  
239:  $Filters = @("Internet Explorer 8", "Internet Explorer 9", "Internet Explorer 10")  
240:  $Updates = @()  
241:    
242:  $LogFile = ProcessLogFile -OperatingSystem $OperatingSystem -Architecture $Architecture  
243:  $Updates = Get-Updates -Architecture $Architecture -Namespace $Namespace -OperatingSystem $OperatingSystem -Updates $Updates  
244:  $Updates = Set-Filters -Filters $Filters -Updates $Updates -UpdateTypes $UpdateTypes  
245:  $Updates = Set-TimeSpanByMonthsOld -MonthsOld 1 -Updates $Updates  
246:  #$Updates = Set-TimeSpanByDatePeriod -StartDate "1/1/2015" -EndDate "12/28/2015" -Updates $Updates  
247:  #$Updates = Set-TimeSpanAllUpdatesBeforeDate -StartDate "9/30/2015" -Updates $Updates  
248:  CreateSoftwareUpdateGroup -OperatingSystem $OperatingSystem -Updates $Updates -Architecture $Architecture  
249:  New-Report -EmailAddressList $EmailAddresses -Updates $Updates -OperatingSystem $OperatingSystem -Architecture $Architecture  
250:  Write-Host  
251:  Write-Host "Total Number of Updates:"$Updates.Count  
252:  $Filters = $null  
253:  $Updates = $null  
254:  $UpdateTypes = $null  
255:    
256:  #Remove Variables  
257:  Remove-Variable -Name Architecture -Scope Local -Force  
258:  Remove-Variable -Name EmailAddresses -Scope Local -Force  
259:  Remove-Variable -Name Filters -Scope Local -Force  
260:  Remove-Variable -Name LogFile -Scope Local -Force  
261:  Remove-Variable -Name Namespace -Scope Local -Force  
262:  Remove-Variable -Name OperatingSystem -Scope Local -Force  
263:  Remove-Variable -Name Updates -Scope Local -Force  
264:  Remove-Variable -Name UpdateTypes -Scope Local -Force  
265:    

08 December 2015

Windows Updates List

Posted By: Mick Pletcher - 5:00 PM












I have been working on writing a new script for SCCM and decided while writing it, I would take one of the functions and make it into a separate script for just retrieving windows updates. Sometimes you need a list to compare against, which is what I am using this for as a reporting tool in another script I am writing.

This script will query the SCCM server for a list of all Microsoft updates for a specific OS. The list can be customized down to a specific architecture, update types, such as security updates and service packs, and the list can filter out such things as IE 8, IE 9, and such.

The first thing is to customize the Namespace variable with your company's sitecode on line 21. Next, you will need to change line 28 to the OS you are wanting to search for. I put a couple of examples beside it, but you can use any OS that is in the All Software Updates list. On line 29, You can enter x86, x64, or null. Such OSs as 2012 R2 don't display a system architecture in the list, so you can use null. For UpdateTypes, you can enter the type of update you want to search for, such as Update or Security Update. If you leave the array blank, it will return all update types. Finally, $Filters lets you filter out updates that contain certain word in the title. I filtered out IE 8, 9, and 10, since my firm uses 11.

The script will then return a list of all updates and then a count of how many were returned.

NOTE: I ran this script from the SCCM server.

You can download this script from here.


WindowsUpdatesList.ps1
1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       12/8/2015 9:08 AM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        WindowsUpdatesList.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will generate a list of Windows updates from a query of the SCCM  
12:            server. You will need to customize the Namespace variable for your SCCM  
13:            server on line 19. Line 27 is where you input the OS you are searching for.  
14:            This can include any OS in the   
15:  #>  
16:    
17:  #Declare Variables  
18:  Set-Variable -Name Architecture -Force  
19:  Set-Variable -Name Filter -Force  
20:  Set-Variable -Name Filters -Force  
21:  Set-Variable -Name Namespace -Value root\sms\site_<sitecode> -Force  
22:  Set-Variable -Name OperatingSystem -Force  
23:  Set-Variable -Name Updates -Force  
24:  Set-Variable -Name UpdateType -Force  
25:  Set-Variable -Name UpdateTypes -Force  
26:    
27:  cls  
28:  $OperatingSystem = "Windows 7" #Windows 7, Windows 8.1, Windows Server 2012 R2  
29:  $Architecture = "x86" #x86, x64, or null  
30:  $UpdateTypes = @() #Security Update, Update, Service Pack  
31:  $Filters = @("Internet Explorer 8", "Internet Explorer 9", "Internet Explorer 10")  
32:    
33:  If ($Architecture -eq "x86") {  
34:       $Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) -and ($_.LocalizedDisplayName -notmatch "x64-based Systems") }  
35:  } elseif ($Architecture -eq "x64") {  
36:       $Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) -and ($_.LocalizedDisplayName -match "x64-based Systems") }  
37:  } else {  
38:       $Updates = Get-WmiObject -Class SMS_SoftwareUpdate -Namespace $Namespace | where-object { ($_.LocalizedDisplayName -match $OperatingSystem) }  
39:  }  
40:  foreach ($Filter in $Filters) {  
41:       $Updates = $Updates | where {$_.LocalizedDisplayName -notmatch $Filter}  
42:  }  
43:  If ($UpdateTypes.Count -ge 1) {  
44:       foreach ($UpdateType in $UpdateTypes) {  
45:            $Updates = $Updates | where { $_.LocalizedCategoryInstanceNames -match $UpdateType }  
46:       }  
47:       If ($UpdateTypes -eq "Update") {  
48:            $Updates = $Updates | where { $_.LocalizedDisplayName -notmatch "Security Update" }  
49:       }  
50:  }  
51:  $Updates.LocalizedDisplayName  
52:  Write-Host  
53:  Write-Host "Total Number of Updates:"$Updates.Count  
54:  $Filters = $null  
55:    
56:  #Remove Variables  
57:  Remove-Variable -Name Architecture -Force  
58:  Remove-Variable -Name Filter -Force  
59:  Remove-Variable -Name Filters -Force  
60:  Remove-Variable -Name Namespace -Force  
61:  Remove-Variable -Name OperatingSystem -Force  
62:  Remove-Variable -Name Updates -Force  
63:  Remove-Variable -Name UpdateType -Force  
64:  Remove-Variable -Name UpdateTypes -Force  
65:    

02 December 2015

Automating Microsoft Endpoint Full System Scan upon Infection with Email Notification

Posted By: Mick Pletcher - 2:56 PM
While helping to manage Microsoft Endpoint, a former colleague suggested that I setup Endpoint to automatically run a full system scan each time an infection is detected. I googled the blog posting on it and although it is a great post, I figured I could streamline it even more by just using SCCM alone to achieve the same outcome. It is nice when you are out of the office and your backup might not have the time to keep an eye on the antivirus infections.

This is a second edition of the previous script I wrote. I decided to leave that script if you do not want to have email notification upon a full system scan. This script includes sending out an email to the specified users in the EmailAddresses.txt file. This file resides in the same directory as the script. The other thing that needs to be done is to define the Installation program in SCCM using psexec.exe. Psexec.exe will also need to reside in the same directory as the PowerShell script. This allows the PowerShell script to be executed under a domain account, thereby giving it the ability to use the send-mailmessage commandlet.  Here is how to do this:

psexec.exe \\%computername% -u <domain>\<username> -p <password> -h cmd.exe /c "echo . | powershell.exe -executionpolicy bypass -file install.ps1"


I decided to use the SCCM custom application detection to scan a system and see if a full system scan has been performed. I first started out by writing a powershell script that would perform a WMI query on the SCCM server for the status of the system the application detection was being run on. The problem I ran across was that the application is being run under system credentials, which would require me to pass network credentials within the script. Instead of having to do this, I decided to query the event viewer logs on the local machine to look for the last infection date/time, which is event 1116. I queried all machines in my firm to find another event log that was unused, and 1118 happened to be just the one.

Here is how the process works:

  1. SCCM deploys the package to the system.
  2. The application detection queries the event viewer logs for the last 1116 ID (infection).
  3. The application detection queries the event viewer logs for the last 1118 ID.
  4. If a system 1118 ID  does not exist since the last infection, or there is no 1116 ID detected, the custom detection method will exit out as a failure.
  5. If the custom detection failed, the antivirusscan.ps1 file will be executed on the machine.
  6. An email is sent that tells a scan was performed on %COMPUTERNAME% with the virus details in the body.
  7. Once the scan is complete, a machine policy update is initiated to update the SCCM server with the status of the system.
  8. The application detection is initiated again to confirm the scan occurred. 


This is setup in SCCM as a normal application deployment. The only thing that differs from a standard deployment is the application detection method. That script is imported in for the detection method. The antivirusscan.ps1 file is setup as the installation program. I have mine entered like this:
powershell.exe -executionpolicy bypass -file antivirusscan.ps1

One more thing is that I have the application hidden from the software center. There really isn't a need for it to be there.

Line 57 on the AntivirusScanEmail.ps1 file is the only line of code you should have to customize.

You can download the application and application detection files from the following links:



AntivirusScanEmail.ps1
1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       11/19/2015 3:26 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        AntiVirusScanEmail.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will initiate a full or quick scan, whichever one is uncommented  
12:            out below. It will then write a log to the event viewer logs showing the   
13:            scan was executed. Next, it will email the designated IT staff telling the   
14:            system scan has been performed. The final step is to execute a machine policy  
15:            update so the SCCM server is updated on the status of the system.  
16:  #>  
17:    
18:  #Declare Variables  
19:  Set-Variable -Name EmailAddress -Force  
20:  Set-Variable -Name EmailAddresses -Force  
21:  Set-Variable -Name LastInfection -Force  
22:  Set-Variable -Name Output -Force  
23:  Set-Variable -Name RelativePath -Force  
24:  Set-Variable -Name SMSwmi -Force  
25:  Set-Variable -Name strAction -Force  
26:  Set-Variable -Name Subject -Force  
27:  Set-Variable -Name WMIPath -Force  
28:    
29:  Import-Module $env:ProgramFiles"\Microsoft Security Client\MpProvider"  
30:  $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
31:  $EmailAddresses = @()  
32:  $EmailAddresses = Get-Content -Path $RelativePath"EmailAddresses.txt"  
33:  $LastInfection = get-winevent -filterhashtable @{ logname = 'system'; ID = 1116 } -maxevents 1 -ErrorAction SilentlyContinue  
34:  <#Full Scan#>  
35:  Start-MProtScan -ScanType "FullScan"  
36:  cls  
37:  Write-Warning "Error: $_"  
38:  Write-Host $_.Exception.ErrorCode  
39:  New-EventLog –LogName System –Source "Antimalware Full Scan"  
40:  If ((Get-EventLog -LogName System -Source "Antimalware Quick Scan") -eq $null) {  
41:       New-EventLog –LogName System –Source "Antimalware Quick Scan"  
42:  }  
43:  Write-EventLog -LogName System -Source "Antimalware Full Scan" -EntryType Information -EventId 1118 -Message "Antimalware full system scan was performed" -Category ""  
44:  $Subject = "Virus Detection Report for" + [char]32 + $env:COMPUTERNAME  
45:  $Output = "An antimalware full system scan has been performed on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
46:    
47:  <#Quick Scan  
48:  Start-MProtScan -ScanType "QuickScan"  
49:  If ((Get-EventLog -LogName System -Source "Antimalware Quick Scan") -eq $null) {  
50:       New-EventLog –LogName System –Source "Antimalware Quick Scan"  
51:  }  
52:  Write-EventLog -LogName System -Source "Antimalware Quick Scan" -EntryType Information -EventId 1118 -Message "Antimalware quick system scan was performed" -Category ""  
53:  $Subject = "Virus Detection Report for" + [char]32 + $env:COMPUTERNAME  
54:  $Output = "An antimalware quick system scan has been performed on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
55:  #>  
56:  foreach ($EmailAddress in $EmailAddresses) {  
57:       Send-MailMessage -To $EmailAddress -From "IT@acme.com" -Subject $Subject -Body $Output -SmtpServer "smtp.acme.com"  
58:  }  
59:  $WMIPath = "\\" + $env:COMPUTERNAME + "\root\ccm:SMS_Client"  
60:  $SMSwmi = [wmiclass]$WMIPath  
61:  $strAction = "{00000000-0000-0000-0000-000000000021}"  
62:  [Void]$SMSwmi.TriggerSchedule($strAction)  
63:    
64:  #Cleanup Variables  
65:  Remove-Variable -Name EmailAddress -Force  
66:  Remove-Variable -Name EmailAddresses -Force  
67:  Remove-Variable -Name LastInfection -Force  
68:  Remove-Variable -Name Output -Force  
69:  Remove-Variable -Name RelativePath -Force  
70:  Remove-Variable -Name SMSwmi -Force  
71:  Remove-Variable -Name strAction -Force  
72:  Remove-Variable -Name Subject -Force  
73:  Remove-Variable -Name WMIPath -Force  
74:    


ApplicationVirusDetectionMethodEmail.ps1

1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       11/19/2015 3:26 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        ApplicationVirusDetectionMethodEmail.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:  #>  
12:    
13:    
14:  $LastInfection = get-winevent -filterhashtable @{ logname = 'system'; ID = 1116 } -maxevents 1 -ErrorAction SilentlyContinue  
15:  $LastFullScan = get-winevent -filterhashtable @{ logname = 'system'; ID = 1118 } -maxevents 1 -ErrorAction SilentlyContinue  
16:  If (($LastFullScan.TimeCreated -lt $LastInfection.TimeCreated) -or ($LastInfection -eq $null)) {  
17:       Start-Sleep -Seconds 5  
18:       exit 0  
19:  } else {  
20:       Write-Host "No Infection"  
21:       Start-Sleep -Seconds 5  
22:       exit 0  
23:  }  
24:    

30 November 2015

Local Administrator Report

Posted By: Mick Pletcher - 2:48 PM










The firm I work at does not give users local administrator access for several reasons. We did an audit of our systems and found out several users had local administrator privileges that should not have. In order to keep track of all systems all of the time, I wrote the following two PowerShell scripts to report systems in which a user is part of the administrators group.

This is a two step process, using SCCM Application deployment. The first step is to use a custom detection method written in PowerShell. The detection script queries the local administrator group of the system it is being executed on. It excludes members in the $MemberExclusions and systems in the $SystemExclusions arrays. I hard coded the exclusions in the detection script so that I can go right in there and directly add users and systems as needed. When you add a system to the application detection method, you also need to add the system to the exclusions.txt file. If there are no users in the administrators group, then the application detection script will return an "installed" status back to SCCM. If there are additional users in the group, besides the excluded, then the script will return a "not installed" message back to SCCM.

The only portion of the LocalAdministratorsDetection.ps1 file you need to modify are lines 19, 26, and 27. 

When a "not installed" message is returned back, which is an exit code 0, SCCM will now execute the LocalAdministrators.ps1 script to "install" the application. I wanted to have emails generated when this script executes, so I had to find a way to execute the script with a domain account, instead of the normal system account. This was achieved by placing a copy of psexec.exe in the same folder as the localadministrators.ps1 file exists. Now the way to set this up is to create a normal application deployment in SCCM 2012. As you setup the deployment types, under the Programs tab, you will need to use the following command line:

psexec.exe \\%COMPUTERNAME% -u <domain>\<domain account> -p <password> -h cmd.exe /c "echo . | powershell.exe -executionpolicy bypass -file \\<Directory>\LocalAdministrators.ps1"

The only parts of the psexec.exe command line you need to customize for your environment will be those highlighted in bold. This command line executes the powershell script using the specified domain account. The %COMPUTERNAME% grabs the computer name of the system it is being executed on.

The LocalAdministrators.ps1 script will now get a list of all users in the administrators group that are not excluded in the exclusions.txt file, which also exists in the same directory as the .ps1 file. You will need to create a file called EmailAddresses.txt, which will have a list of email addresses that you want to be emailed a report. You also need to create an exclusions.txt file. This will have a list of systems to exclude from the query. The only parts of the localadministrators.ps1 file you need to customize are Lines 27, 28, 77, and 83. At the end of the script, it will send out an email to all addresses in the EmailAddresses.txt file. Finally, it renames the file LocalAdministrators.log to LocalAdministrators_Emailed.log. When the application detection is rerun, it will see the _Emailed in the filename and return a success if there are extra users in the local administrator group. If there are no extra users in the group and that file exists, then the application detection method will delete the .log file. 

You can download the LocalAdministrators.ps1 script from here.
You can download the LocalAdministratorsDetection.ps1 script from here.


LocalAdministrators.ps1
1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       11/23/2015 1:14 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        LocalAdministrators.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will open query the local administrators group. It generates  
12:            a log file if there are users in the local administrators group that are  
13:            not in the exclusions group. A .log file is written to the local HDD. The  
14:            script then returns a error code 0 back to SCCM, which will initiate a   
15:            software deployment. At that point, the secondary script will email  
16:            the .log file to the appropriate users. That script then deletes the  
17:            .log file which will then create a successful   
18:  #>  
19:    
20:  #Declare Global Variables  
21:  Set-Variable -Name Body -Force  
22:  Set-Variable -Name EmailAddress -Force  
23:  Set-Variable -Name EmailAddresses -Force  
24:  Set-Variable -Name Exclusions -Force  
25:  Set-Variable -Name LocalAdmin -Force  
26:  Set-Variable -Name LocalAdmins -Force  
27:  Set-Variable -Name LogFile -Value $env:windir"\Logs\LocalAdministrators.log" -Force  
28:  Set-Variable -Name LogFileEmailed -Value $env:windir"\Logs\LocalAdministrators_Emailed.log" -Force  
29:  Set-Variable -Name Member -Force  
30:  Set-Variable -Name Members -Force  
31:  Set-Variable -Name Output -Force  
32:  Set-Variable -Name Prof -Force  
33:  Set-Variable -Name Profiles -Force  
34:  Set-Variable -Name RelativePath -Force  
35:    
36:  cls  
37:  $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
38:  $Body = "Local Administrator(s)" + [char]13 + "---------------------------" + [char]13  
39:  $EmailAddresses = @()  
40:  $EmailAddresses = Get-Content -Path $RelativePath"EmailAddresses.txt"  
41:  $LocalAdmins = @()  
42:  $Members = net localgroup administrators | where { $_ -AND $_ -notmatch "command completed successfully" } | select -skip 4  
43:  $Profiles = Get-ChildItem -Path $env:SystemDrive"\users" -Force  
44:  $Exclusions = Get-Content -Path $RelativePath"Exclusions.txt"  
45:  Foreach ($Member in $Members) {  
46:       $Member = $Member.Split("\")  
47:       If ($Member.Count -gt 1) {  
48:            [string]$Member = $Member[1]  
49:            If ($Member -notin $Exclusions) {  
50:                 Foreach ($Prof in $Profiles) {  
51:                      If ($Member -eq $Prof) {  
52:                           $LocalAdmins += $Member  
53:                      }  
54:                 }  
55:            }  
56:       }  
57:       Remove-Variable -Name Member  
58:  }  
59:  if ((Test-Path $LogFileEmailed) -eq $true) {  
60:       Remove-Item -Path $LogFileEmailed -Force  
61:  }  
62:  if ((Test-Path $LogFile) -eq $true) {  
63:       Remove-Item -Path $LogFile -Force  
64:  }  
65:  if ($LocalAdmins.Count -gt 0) {  
66:       if ((Test-Path $LogFile) -eq $false) {  
67:            New-Item -Path $LogFile -ItemType file -Force  
68:       }  
69:       foreach ($LocalAdmin in $LocalAdmins) {  
70:            Add-Content -Path $LogFile -Value $LocalAdmin -Force  
71:            $Body = $Body + $LocalAdmin + [char]13  
72:       }  
73:  }  
74:  If ($LocalAdmins.count -eq 1) {  
75:       $Output = $LocalAdmin + [char]32 + "is a local administrator on" + [char]32 + $env:COMPUTERNAME  
76:       foreach ($EmailAddress in $EmailAddresses) {  
77:            Send-MailMessage -To $EmailAddress -From "IT@acme.com" -Subject "Local Administrator Report" -Body $Output -SmtpServer "smtp.acme.com"  
78:       }  
79:       Rename-Item -Path $LogFile -NewName $LogFileEmailed -Force  
80:  } else {  
81:       $Output = "The attached file lists all local administrators on" + [char]32 + $env:COMPUTERNAME  
82:       foreach ($EmailAddress in $EmailAddresses) {  
83:            Send-MailMessage -To $EmailAddress -From "IT@acme.com" -Subject "Local Administrator Report" -Body $Output -Attachments $LogFile -SmtpServer "smtp.acme.com"  
84:       }  
85:       Rename-Item -Path $LogFile -NewName $LogFileEmailed -Force  
86:  }  
87:  $LocalAdmins = $null  
88:    
89:  #Cleanup Global Variables  
90:  Remove-Variable -Name Body -Force  
91:  Remove-Variable -Name EmailAddress -Force  
92:  Remove-Variable -Name EmailAddresses -Force  
93:  Remove-Variable -Name Exclusions -Force  
94:  Remove-Variable -Name LocalAdmin -Force  
95:  Remove-Variable -Name LocalAdmins -Force  
96:  Remove-Variable -Name LogFile -Force  
97:  Remove-Variable -Name LogFileEmailed -Force  
98:  Remove-Variable -Name Members -Force  
99:  Remove-Variable -Name Output -Force  
100:  Remove-Variable -Name Prof -Force  
101:  Remove-Variable -Name Profiles -Force  
102:    



LocalAdministratorsDetection.ps1
1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.98  
5:        Created on:       11/23/2015 1:14 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:        LocalAdministratorsDetectionMethod.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will query the local administrators group. It will return a  
12:            success to SCCM if there are no members in the local administrators   
13:            group or if a system is in the SystemExclusions array or a user is   
14:            in the MemberExclusions variable.   
15:  #>  
16:    
17:  #Declare Global Variables  
18:  Set-Variable -Name LocalAdmins -Force  
19:  Set-Variable -Name LogFile -Value $env:windir"\Logs\LocalAdministrators_Emailed.log" -Force  
20:  Set-Variable -Name Member -Force  
21:  Set-Variable -Name MemberExclusions -Force  
22:  Set-Variable -Name Members -Force  
23:  Set-Variable -Name SystemExclusions -Force  
24:    
25:  cls  
26:  $MemberExclusions = @("Domain Admins","Workstation Admins")  
27:  $SystemExclusions = @("SYSTEM01")  
28:  $LocalAdmins = @()  
29:  $Members = net localgroup administrators | where { $_ -AND $_ -notmatch "command completed successfully" } | select -skip 4  
30:  $Profiles = Get-ChildItem -Path $env:SystemDrive"\users" -Force  
31:  Foreach ($Member in $Members) {  
32:       $Member = $Member.Split("\")  
33:       If ($Member.Count -gt 1) {  
34:            [string]$Member = $Member[1]  
35:            If ($Member -notin $MemberExclusions) {  
36:                 $LocalAdmins += $Member  
37:            }  
38:       }  
39:       Remove-Variable -Name Member  
40:  }  
41:  if (($LocalAdmins.Count -eq 0) -and ((Test-Path -Path $LogFile) -eq $true)) {  
42:       Remove-Item -Path $LogFile -Force  
43:  }  
44:  if (($LocalAdmins.Count -gt 0) -and ($env:COMPUTERNAME -notin $SystemExclusions) -and ((Test-Path -Path $LogFile) -eq $false )) {  
45:       Start-Sleep -Seconds 5  
46:       exit 0  
47:  } else {  
48:       Write-Host "No Local Administrators"  
49:       Start-Sleep -Seconds 5  
50:       exit 0  
51:  }  
52:  $LocalAdmins = $null  
53:    
54:  #Cleanup Global Variables  
55:  Remove-Variable -Name LocalAdmins -Force  
56:  Remove-Variable -Name LogFile -Force  
57:  Remove-Variable -Name Member -Force  
58:  Remove-Variable -Name MemberExclusions -Force  
59:  Remove-Variable -Name Members -Force  
60:  Remove-Variable -Name SystemExclusions -Force  
61:    

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