29 December 2015

Microsoft Office Updater

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.


There is a newer version of this script located 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

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

There is a newer tool located here


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

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

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:    

20 November 2015

Automating Microsoft Endpoint Full System Scan upon Infection

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.

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. Once the scan is complete, a machine policy update is initiated to update the SCCM server with the status of the system.
  7. 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. 
Click here to download the Application Virus Detection Method.
Click here to download the Antivirus Scan script.

Antivirus Scan Script
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:        Filename:        AntiVirusScan.ps1  
8:       ===========================================================================  
9:       .DESCRIPTION  
10:            This script will initiate a full or quick scan, whichever one is uncommented  
11:            out below. It will then write a log to the event viewer logs showing the   
12:            scan was executed. The final step is to execute a machine policy update so  
13:            the SCCM server is updated on the status of the system.  
14:  #>  
15:    
16:  Import-Module $env:ProgramFiles"\Microsoft Security Client\MpProvider"  
17:  <#Full Scan#>  
18:  Start-MProtScan -ScanType "FullScan"  
19:  New-EventLog –LogName System –Source "Antimalware Full Scan"  
20:  Write-EventLog -LogName System -Source "Antimalware Full Scan" -EntryType Information -EventId 1118 -Message "Antimalware full system scan was performed" -Category ""  
21:    
22:  <#Quick Scan  
23:  Start-MProtScan -ScanType "QuickScan"  
24:  New-EventLog –LogName System –Source "Antimalware Quick Scan"  
25:  Write-EventLog -LogName System -Source "Antimalware Quick Scan" -EntryType Information -EventId 1118 -Message "Antimalware quick system scan was performed" -Category ""  
26:  #>  
27:    
28:  $WMIPath = "\\" + $env:COMPUTERNAME + "\root\ccm:SMS_Client"  
29:  $SMSwmi = [wmiclass]$WMIPath  
30:  $strAction = "{00000000-0000-0000-0000-000000000021}"  
31:  [Void]$SMSwmi.TriggerSchedule($strAction)  
32:  Exit 0  
33:    


Application Virus Detection Method
1:  $LastInfection = get-winevent -filterhashtable @{ logname = 'system'; ID = 1116 } -maxevents 1 -ErrorAction SilentlyContinue  
2:  $LastFullScan = get-winevent -filterhashtable @{ logname = 'system'; ID = 1118 } -maxevents 1 -ErrorAction SilentlyContinue  
3:  If (($LastFullScan.TimeCreated -lt $LastInfection.TimeCreated) -or ($LastInfection -eq $null)) {  
4:       Start-Sleep -Seconds 5  
5:       exit 0  
6:  } else {  
7:       Write-Host "No Infection"  
8:       Start-Sleep -Seconds 5  
9:       exit 0  
10:  }  
11:    

05 November 2015

Deploying Microsoft Endpoint

I recently converted my firm to Microsoft Endpoint. Part of the process is including endpoint in the golden image. I wrote this powershell script that will install endpoint and then remove the necessary registry keys so it will set itself back up when the reference image is laid down on a new machine. The script also allows you to visually see if the application is installed correctly by returning a success/failure by checking to see if MsMpEng.exe is running. You may wonder why I have an uninstall first. I do this in all of my installation scripts in the event something is wrong with the currently installed app and it needs to be reinstalled. You can easily comment out that line if you do not want that to occur.

I execute this script using psexec so that it is run under the local system context. I use the following:
psexec.exe \\%computername% -s -h cmd.exe /c "echo . | powershell.exe -executionpolicy bypass -file install_build.ps1"

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:       05 November 2015 10:37 AM  
6:        Created by:       Mick Pletcher  
7:        Organization:        
8:        Filename:        installEndPoint_build.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            Install endpoint during the generation of a golden image. This will  
12:            also remove all necessary registry keys required in preparation of   
13:            generating a golden image.  
14:  #>  
15:    
16:  #Declare Global Memory  
17:  $Global:RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
18:    
19:  Function Wait-ProcessEnd {  
20:       <#  
21:       .SYNOPSIS  
22:            Wait-Process  
23:       .DESCRIPTION  
24:            Waits for a Process to end before continuing.  
25:       #>  
26:         
27:       Param ([String]$Process)  
28:       $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
29:       If ($Proc -ne $null) {  
30:            Do {  
31:                 Start-Sleep -Seconds 5  
32:                 $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
33:            } While ($Proc -ne $null)  
34:       }  
35:  }  
36:    
37:  Function Install-EXE {  
38:       <#  
39:       .SYNOPSIS  
40:            Install-EXE  
41:       .DESCRIPTION  
42:            Installs an EXE file  
43:       #>  
44:         
45:       Param ([String]$DisplayName,  
46:            [String]$Executable,  
47:            [String]$Switches)  
48:       Write-Host "Install"$DisplayName"....." -NoNewline  
49:       If ((Test-Path $Executable) -eq $true) {  
50:            Start-Process -FilePath $Executable -ArgumentList $Switches  
51:            Wait-ProcessEnd -Process "scepinstall"  
52:       } else {  
53:            $ErrCode = 1  
54:       }  
55:       $Process = Get-Process -ProcessName MsMpEng -ErrorAction SilentlyContinue  
56:       If ($Process.ProcessName -eq "MsMpEng") {  
57:            Write-Host "Success" -ForegroundColor Yellow  
58:       } else {  
59:            Write-Host "Failed" -ForegroundColor Red  
60:       }  
61:  }  
62:    
63:  Function Uninstall-EXE {  
64:       <#  
65:       .SYNOPSIS  
66:            Uninstall-EXE  
67:       .DESCRIPTION  
68:            Uninstalls an EXE file  
69:       #>  
70:         
71:       Param ([String]$DisplayName,  
72:            [String]$Executable,  
73:            [String]$Switches)  
74:       Write-Host "Uninstall"$DisplayName"....." -NoNewline  
75:       If ((Test-Path $Executable) -eq $true) {  
76:            Start-Process -FilePath $Executable -ArgumentList $Switches  
77:            Wait-ProcessEnd -Process "scepinstall"  
78:       }  
79:       $Process = Get-Process -ProcessName MsMpEng -ErrorAction SilentlyContinue  
80:       If ($Process -eq $null) {  
81:            Write-Host "Success" -ForegroundColor Yellow  
82:       } else {  
83:            Write-Host "Failed" -ForegroundColor Red  
84:       }  
85:  }  
86:    
87:  Function Remove-RegistryValue {  
88:       <#  
89:       .SYNOPSIS  
90:            Remove-RegistryValue  
91:       .DESCRIPTION  
92:            Deletes a specific registry value  
93:       .EXAMPLE  
94:            Remove-RegistryValue "HKEY_LOCAL_MACHINE\SOFTWARE\Hummingbird"  
95:       #>  
96:         
97:       Param ([String]$RegistryKey,  
98:            [String]$Value)  
99:       $tempdrive = New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT  
100:       $RegistryKey1 = $RegistryKey.split("\")  
101:       switch ($RegistryKey1[0]) {  
102:            "HKEY_CLASSES_ROOT" { $RegistryKey1[0] = "HKCR" }  
103:            "HKEY_CURRENT_USER" { $RegistryKey1[0] = "HKCU" }  
104:            "HKEY_LOCAL_MACHINE" { $RegistryKey1[0] = "HKLM" }  
105:            "HKEY_USERS" { $RegistryKey1[0] = "HKU" }  
106:            "HKEY_CURRENT_CONFIG" { $RegistryKey1[0] = "HKCC" }  
107:       }  
108:       For ($i = 0; $i -lt $RegistryKey1.Count; $i++) {  
109:            $RegKey = $RegKey + $RegistryKey1[$i]  
110:            If ($i -eq 0) {  
111:                 $RegKey = $RegKey + ":\"  
112:            } elseif ($i -ne $RegistryKey1.Count - 1) {  
113:                 $RegKey = $RegKey + "\"  
114:            } else {  
115:                 $RegKey = $RegKey  
116:            }  
117:       }  
118:       Write-Host "Delete"$RegKey"\"$Value"....." -NoNewline  
119:       $exists = Get-ItemProperty -Path $RegKey -Name $Value -ErrorAction SilentlyContinue  
120:       If (($exists -ne $null) -and ($exists.Length -ne 0)) {  
121:            Remove-ItemProperty -Path $RegKey -Name $Value -Force  
122:       }  
123:       $exists = Get-ItemProperty -Path $RegKey -Name $Value -ErrorAction SilentlyContinue  
124:       If ($exists -eq $null) {  
125:            Write-Host "Success" -ForegroundColor Yellow  
126:       } else {  
127:            Write-Host "Failed" -ForegroundColor Yellow  
128:       }  
129:  }  
130:    
131:  cls  
132:  Uninstall-EXE -DisplayName "Microsoft Endpoint" -Executable $global:RelativePath"scepinstall.exe" -Switches "/u /s"  
133:  $Parameters = "/s /policy " + $global:RelativePath + "EndpointPolicies.xml"  
134:  Install-EXE -DisplayName "Microsoft Endpoint" -Executable $global:RelativePath"scepinstall.exe" -Switches $Parameters  
135:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware" -Value "InstallTime"  
136:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Scan" -Value "LastScanRun"  
137:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Scan" -Value "LastScanType"  
138:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Scan" -Value "LastQuickScanID"  
139:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft Antimalware\Scan" -Value "LastFullScanID"  
140:  Remove-RegistryValue -RegistryKey "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\RemovalTools\MRT" -Value "GUID"  
141:    

04 November 2015

.Net Framework 4.5.x Error 0x80004005

Recently, I had to deploy .Net Framework 4.5.2 to all systems, as it was a requirement for a software update. I first deployed it through software updates in SCCM 2012. The status showed up as compliant. I thought nothing more of it. I went to deploy the application and it failed on many machines. SCCM was reporting back false data, which is part of my other blog posting. I had verified the powershell script I had written to install the app was working when running it from the software center and/or a command line. I was perplexed. I then kicked off an install after rebooting a machine through a machine policy update from the SCCM console. It stuck on the NDP452-KB2901907-x86-x64-AllOS-ENU.exe execution. I finally found the log file for this located at c:\windows\temp\dd_NDP452-KB2901907-x86-x64-AllOS-ENU_decompression_log.txt. In the log were these last three lines:

[11/4/2015, 14:4:24] Extracting files to: C:\4ace42dcab1cf4386b698f\
[11/4/2015, 14:4:24] Error 0x80004005: Failed to extract all files out of box container #0.
[11/4/2015, 14:4:24] Error 0x80004005: Failed to extract

I then tried another step by checking off the Run installation and uninstall program as 32-bit process on 64-bit clients. This did no good. Since it would successfully execute from a command line and/or manual execution through software center, I decided to add it as package and run it directly from the network share instead of a distribution point. This worked. It is now installing with no problems. When creating the package, there will be no source files. Instead, you will put the network share of where the installation files exist in the Start in field. This should allow you to now install 4.5.2. 

03 November 2015

Packaging EverMap Redaction for Adobe Acrobat XI

To easily deploy Redaction for Adobe Acrobat XI, I created an MSI file using the AppDeploy Repackager. There are a total of 8 files it packages up. My firm has an enterprise license, which gets written to the AutoRedact.LIC file. To properly package this up, you can download the attached XML file I have made available that will generate the MSI using AppDeploy Repackager. You will need to install the EverMap application using your enterprise key on the same machine as Adobe Acrobat Standard XI and the repackager are installed on.

Another thing you can do is to pre-configure the preferences in the Redacting plugin. To do this, open Acrobat up, click Plug-Ins-->Redacting-->Preferences. Make whatever changes that are needed for all users. Click the Exemption Codes tab. Click Create New. You can lable the Group Title anything you prefer. I left it at Default. Click Add Code. I used REDACTED under Use this code for redacting. In the description field, I used Redaction. Click OK. Click OK. Click OK. This will create three CFS files located %APPDATA%\EverMap\AutoRedact. Create a GPO that will push these three files out to all systems with the Redaction app installed using a user policy. I limited this in the GPO via MSI GUID.

You can download the XML from here.

Here is what the XML looks like. The unins000.xxx files are forced to be created by AppDeploy.



 <?xml version="1.0" encoding="UTF-16"?>  
 <Repackager>  
      <ProjectFile Version="1.1">  
      </ProjectFile>  
      <Repackager Version="1.2">  
      </Repackager>  
      <ProductInformation>  
           <ProductInformation ApplicationName="AutoRedact Plug-In" Version="1.9.6.0" CompanyName="EverMap Company, LLC.">  
           </ProductInformation>  
      </ProductInformation>  
      <FileSystem>  
           <AddedItems>  
                <DirectoryList>  
                     <Directory Name="%PROGRAMFILES%\Adobe\Acrobat 11.0\Acrobat\plug_ins\AutoRedact">  
                          <File Name="unins000.exe">  
                          </File>  
                          <File Name="unins000.dat">  
                          </File>  
                          <File Name="LanguageTable.lan">  
                          </File>  
                          <File Name="AutoRedactManual.pdf">  
                          </File>  
                          <File Name="AutoRedactIntro.pdf">  
                          </File>  
                          <File Name="AutoRedact.api">  
                          </File>  
                          <File Name="AutoRedact.LIC">  
                          </File>  
                          <File Name="AUTOREDACT_RegExp.txt">  
                          </File>  
                          <File Name="AUTOREDACTDict.CFS">  
                          </File>  
                          <File Name="AUTOREDACTCodes.CFS">  
                          </File>  
                     </Directory>  
                </DirectoryList>  
           </AddedItems>  
           <DeletedItems>  
                <DirectoryList>  
                </DirectoryList>  
           </DeletedItems>  
           <ModifiedItems>  
                <DirectoryList>  
                </DirectoryList>  
           </ModifiedItems>  
      </FileSystem>  
      <RegistrySystem>  
           <AddedItems>  
                <SubKeyList>  
                </SubKeyList>  
           </AddedItems>  
           <DeletedItems>  
                <SubKeyList>  
                </SubKeyList>  
           </DeletedItems>  
           <ModifiedItems>  
                <SubKeyList>  
                </SubKeyList>  
           </ModifiedItems>  
      </RegistrySystem>  
      <INIFileSystem>  
           <AddedItems>  
                <FileList>  
                     <File Name="%PROGRAMFILES%\AppDeploy\Repackager\AppDeployRepackager.ini">  
                          <Section Name="SnapshotDetails" Key="IsSnapshotTaken" Value="1">  
                          </Section>  
                     </File>  
                </FileList>  
           </AddedItems>  
           <DeletedItems>  
                <FileList>  
                </FileList>  
           </DeletedItems>  
           <ModifiedItems>  
                <FileList>  
                </FileList>  
           </ModifiedItems>  
      </INIFileSystem>  
      <ShortcutList>  
           <Shortcut Name="unins000" ExeName="unins000.exe" Directory="ProgramMenuFolder" Icon="%PROGRAMFILES%\Adobe\Acrobat 11.0\Acrobat\plug_ins\AutoRedact\unins000.exe" IconIndex="0">  
           </Shortcut>  
      </ShortcutList>  
      <ShortcutICONs>  
      </ShortcutICONs>  
 </Repackager>  
   

30 October 2015

SCCM deployment with Error 1612

I was deploying a package earlier today. It took a few iterations to get everything correct. All of a sudden on the fourth update of the package, it would no longer install. It continuously failed. I thought maybe something was wrong with the installer that got downloaded to the machine, so I cleared out the cache. The issue persisted after SCCM re-downloaded the package. I then ran the install manually from the ccmcache folder where it was stored. Everything installed perfectly. I then deleted the application completely from SCCM and re-deployed it as a Package with the same issue. I was stumped at this point. All of my msi installations include a verbose log written to a specific directory on each machine. When I looked at the file, the first thing that was indicated in red was SOURCEMGMT: Failed to resolve source. I scrolled up and all of a sudden it popped out to me when I saw SOURCEMGMT: Trying source C:\windows\ccmcache\1t\emsProfiler\. That directory no longer existed. I knew this because I had manually run the installed from a different directory in the CCMCACHE. At this point, I thought that had to be stored in the registry and it was. It was located under HKEY_USERS\.DEFAULT\Software\Microsoft\Installer\Products\{GUID}\SourceList.

I deleted HKEY_USERS\.DEFAULT\Software\Microsoft\Installer\Products\{GUID}, which included everything underneath it. I reran the installation and it installed correctly. 

Failed to connect to remote distribution point

We recently did a complete upgrade on all equipment in one of our remote offices. After the upgrade, I had to setup a new distribution point. I began by using Prajwal Desai's blog post on setting up a remote distribution point. I went through all of his process three times and still no sucess. It was constantly returning ERROR DPConnection::ConnectWMI() - Failed to connect to  <server>. error = 0x800706ba. Other suggestions on this error had been to check the firewall and make sure certain IIS components were enabled. Finally, I ran across this post that fixed it. When I added the primary SCCM server name to the local administrators group on the remote distribution site server, it fixed it. It was now able to install the DP with no problems.

19 October 2015

Missing Windows Updates in Windows 7 and Windows 2008 R2

Recently, we had a software deployment that required .Net Framework 4.5.2. I deployed the .Net update via SCCM 2012 Software Updates. Within a day, it showed most all systems were now compliant. We were ready to begin testing the deployment of the application upgrade. My colleague came back to me the next day and said the app would not install because 4.5.2 was missing. I began investigating and SCCM still showed it was compliant. We found more and more systems which were missing the .Net. After finally realizing it was not anything that I might have done wrong in the update deployment, I went to a system and manually ran the Windows updates. It was at this point I saw the system was 88 updates behind. The SCCM client had been sending false data back to the SCCM server. I spent much of the evening investigating and finally ran across KB3050265. I downloaded the patch and installed it on my test machine. Low and behold, a bunch of updates appeared in Software Center after running a Software Updates Scan Cycle and a Software Updates Deployment Evaluation Cycle. Had we not been deploying an application that required .Net 4.5.2, we could have gone a long time thinking the updates were deployed. This issue is sporadic, at least in my firm. Some machines were fully patched, while others were not.

NOTE: This was superseded by KB3102810 last November and then by KB313

18 September 2015

Font Installation Script

I had a bunch of fonts that need to be installed during the build of  the golden image. I wrote this script to install them as a task sequence, but also to be able to deploy the fonts easily through SCCM. This script will install all fonts of the specified font type from the specified directory location. All of the fonts you want installed are to be placed in the specified directory location. The script will  then read all files in that location and filter out for the specified font type. It will then install each font individually and output the status on whether it installed or not.

I did write an older font installation script back in 2012 in vbscript located in this blog posting.

You can download the script from here.


 <#       
      .NOTES  
      ===========================================================================  
       Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.93  
       Created on:       9/18/2015 9:44 AM  
       Created by:       Mick Pletcher  
       Organization:        
       Filename:        install.ps1  
      ===========================================================================  
      .DESCRIPTION  
           This script will install all fonts of the specified font type from  
           the specified directory location. All of the fonts you want installed  
           are to be placed in the specified directory location. The script will  
           then read all files in that location and filter out for the specified  
           font type. It will then install each font individually and output  
           the status on whether it installed or not.   
 #>  
   
 function Get-RelativePath {  
      <#  
      .SYNOPSIS  
           Get-RelativePath  
      .DESCRIPTION  
           Defines the path which this script is being executed from  
      .EXAMPLE  
           $RelativePath = Get-RelativePath  
      #>  
        
      #Declare Local Variables  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $RelativePath  
        
      #Cleanup Local Variables  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 Function Install-Fonts {  
      <#  
      .SYNOPSIS  
           Install-Fonts  
      .DESCRIPTION  
           Installs all fonts in the designated source directory.  
      .EXAMPLE  
           Install-Fonts -SourceDirectory "c:\Fonts" -FontType "ttf"  
      #>  
        
      Param ([String]  
           $SourceDirectory,  
           [String]  
           $FontType)  
        
      #Define Local Variables  
      Set-Variable -Name File -Scope Local -Force  
      Set-Variable -Name Files -Scope Local -Force  
      Set-Variable -Name Fonts -Scope Local -Force  
      Set-Variable -Name i -Scope Local -Force  
      Set-Variable -Name sa -Scope Local -Force  
        
      $FontType = "*." + $FontType  
      $sa = new-object -comobject shell.application  
      $Fonts = $sa.NameSpace(0x14)  
      $Files = Get-ChildItem $SourceDirectory -Filter $FontType  
      For ($i = 0; $i -lt $Files.Count; $i++) {  
           $Output = $Files[$i].Name + "....."  
           Write-Host $Files[$i].Name"....." -NoNewline  
           $File = $Env:windir + "\Fonts\" + $Files[$i].Name  
           If ((Test-Path $File) -eq $false) {  
                $Fonts.CopyHere($Files[$i].FullName)  
                If ((Test-Path $File) -eq $true) {  
                     Write-Host "Installed" -ForegroundColor Yellow  
                } else {  
                     Write-Host "Failed" -ForegroundColor Red  
                }  
           } else {  
                Write-Host "Installed" -ForegroundColor Yellow  
           }  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name File -Scope Local -Force  
      Remove-Variable -Name Files -Scope Local -Force  
      Remove-Variable -Name Fonts -Scope Local -Force  
      Remove-Variable -Name i -Scope Local -Force  
      Remove-Variable -Name sa -Scope Local -Force  
 }  
   
 #Declare Local Variables  
 Set-Variable -Name RelativePath -Scope Local -Force  
   
 cls  
 $RelativePath = Get-RelativePath  
 Install-Fonts -SourceDirectory $RelativePath -FontType "ttf"  
 Install-Fonts -SourceDirectory $RelativePath -FontType "otf"  
   
 #Cleanup Local Variables  
 Remove-Variable -Name RelativePath -Scope Local -Force  

02 September 2015

Using PowerShell to retrieve the Bitlocker Recovery Key from Active Directory

I wanted an easy way to find the bitlocker recovery key in the even MBAM was down and as a backup. We use MBAM, but also use Active Directory as a backup for the keys. This script makes it easy to find the key without having to go into AD. I tested this script on user profiles both with and without permissions to the bitlocker recovery keys. If a user does not have permissions to view the key, then the script returns the message no key exists.

This script will prompt for the computer name. It will then display the bitlocker recovery key stored in Active Directory. In order for this to work correctly, you will need to install Remote Server Administration Tools and active the following feature: Remote Server Administration Tools-->
Role Administration Tools-->AD DS and AD LDS Tools-->Active Directory Module for Windows PowerShell.

You can download the script from here.


 <#       
      .NOTES  
      ===========================================================================  
       Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.92  
       Created on:       8/25/2015 1:25 PM  
       Created by:       Mick Pletcher  
       Organization:        
       Filename:        BitlockerRecoveryKey.ps1  
      ===========================================================================  
      .DESCRIPTION  
           This script will prompt for the computer name. It will then display the  
     bitlocker recovery key. In order for this to work correctly, you will   
     need to install Remote Server Administration Tools and active the  
     following feature: Remote Server Administration Tools-->  
     Role Administration Tools-->AD DS and AD LDS Tools-->  
     Active Directory Module for Windows PowerShell.   
 #>  
   
 Function Get-ComputerName {  
   #Declare Local Variables  
   Set-Variable -Name ComputerName -Scope Local -Force  
   
   $ComputerName = Read-Host "Enter the computer name"  
   Return $ComputerName  
   
   #Cleanup Local Variables  
   Remove-Variable -Name ComputerName -Scope Local -Force  
 }  
   
 Function Get-BitlockeredRecoveryKey {  
   param ([String]$ComputerName)  
   
   #Declare Local Variables  
   Set-Variable -Name BitLockerObjects -Scope Local -Force  
   Set-Variable -Name BitLockerRecoveryKey -Scope Local -Force  
   Set-Variable -Name Computer -Scope Local -Value $null -Force  
   Set-Variable -Name System -Scope Local -Force  
   
   $BitLockerObjects = Get-ADObject -Filter { objectclass -eq 'msFVE-RecoveryInformation' }  
   foreach ($System in $BitLockerObjects) {  
     $System = $System.DistinguishedName  
     $System = $System.Split(',')  
     $System = $System[1]  
     $System = $System.Split('=')  
     $System = $System[1]  
     If ($System -eq $ComputerName) {  
       $Computer = Get-ADComputer -Filter {Name -eq $System}  
       $BitLockerRecoveryKey = Get-ADObject -Filter { objectclass -eq 'msFVE-RecoveryInformation' } -SearchBase $Computer.DistinguishedName -Properties 'msFVE-RecoveryPassword'  
       Write-Host "Computer Name:"$System  
       Write-Host "Bitlocker Recovery Key:"$BitLockerRecoveryKey.'msFVE-RecoveryPassword'  
     }  
   }  
   If ($Computer -eq $null) {  
     Write-Host "No recovery key exists" -ForegroundColor Red  
   }  
   
   #Cleanup Local Variables  
   Remove-Variable -Name BitLockerObjects -Scope Local -Force  
   Remove-Variable -Name BitLockerRecoveryKey -Scope Local -Force  
   Remove-Variable -Name Computer -Scope Local -Force  
   Remove-Variable -Name System -Scope Local -Force  
 }  
   
 #Declare Local Variables  
 Set-Variable -Name SystemName -Scope Local -Force  
   
 cls  
 Import-Module ActiveDirectory -Scope Global -Force  
 $SystemName = Get-ComputerName  
 Get-BitlockeredRecoveryKey -ComputerName $SystemName  
   
 #Cleanup Local Variables  
 Remove-Variable -Name SystemName -Scope Local -Force  
   

25 August 2015

Missing Bitlocker Recovery Keys Reporting Tool

In a perfect environment, GPO forces the bitlocker recovery keys into AD. This doesn't always function correctly. Sometimes a system is bitlockered offline and the key isn't there. Other times, a system might be decrypted and they forget to re-encrypt it. This script is written to generate a report on all systems that have not reported a bitlocker recovery key to AD. If a firm has MBAM, then this is likely not needed.

This script will query SCCM for a list of laptop systems. It will then query active directory for a list of bitlockered systems. The script then compares the laptop list with the bitlockered systems list and generates an excel report of systems which do not have a bitlocker key stored in active directory. This can happen when a system is manually encrypted. This script must be executed on the SCCM server in order for this to execute. It has to load the SCCM module, which can only be done from the server. The $OutputFile and $Path are used to specify where to save the excel report and what to name the file.

This can be either run manually, or you can implement this to run from Orchestrator on a scheduled basis. I wrote the script so that if all bitlockered systems have the recovery key present in AD, then an excel report is not generated. Orchestrator looks for the excel spreadsheet. If it is not present, then it does nothing. If it is present, then it emails that spreadsheet to the appropriate management.

NOTE: In order for this script to function, you will need to have the powershell command line active directory enabled on the SCCM server.

The only changes you should have to make are the following:

  • Line 38 if you want to prepopulate the location for saving the csv file
  • Line 56 to set the site server code for your SCCM server
  • Line 136 to point to wherever your SCCM module resides

You can download the script from here.


1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.92  
5:        Created on:       8/25/2015 1:25 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:        
8:        Filename:        MissingBitlockerKeys.ps1  
9:       ===========================================================================  
10:       .DESCRIPTION  
11:            This script will query SCCM for a list of laptop systems. It will then  
12:      query active directory for a list of bitlockered systems. The script   
13:      then compares the laptop list with the bitlockered systems list and  
14:      generates an excel report of systems which do not have a bitlocker  
15:      key stored in active directory. This can happen when a system is   
16:      manually encrypted. This script must be executed on the SCCM server  
17:      in order for this to execute. It has to load the SCCM module, which  
18:      can only be done from the server. The $OutputFile and $Path are used  
19:      to specify where to save the excel report and what to name the file.  
20:    
21:      In order for this script to function, you will need to have the   
22:      powershell command line active directory enabled.   
23:        
24:      This can be either run manually, or you can implement this to run  
25:      from Orchestrator on a scheduled basis. I wrote the script so that  
26:      if all bitlockered systems have the recovery key present in AD, then  
27:      an excel report is not generated. Orchestrator looks for the excel  
28:      spreadsheet. If it is not present, then it does nothing. If it is  
29:      present, then it emails that spreadsheet to the appropriate   
30:      management.  
31:  #>  
32:    
33:  param  
34:  (  
35:       [string]  
36:       $OutputFile = 'MissingBitlockerKeys.csv',  
37:       [string]  
38:       $Path  
39:  )  
40:    
41:  function ProcessTextFile {  
42:       If ((Test-Path -Path $OutputFile) -eq $true) {  
43:            Remove-Item -Path $OutputFile -Force  
44:       }  
45:  }  
46:    
47:    
48:  function Get-Laptops {  
49:       #Declare Local Variables  
50:       Set-Variable -Name Item -Scope Local -Force  
51:       Set-Variable -Name QuerySystems -Scope Local -Force  
52:       Set-Variable -Name Systems -Scope Local -Force  
53:       Set-Variable -Name WQL -Scope Local -Force  
54:         
55:       $QuerySystems = @()  
56:       Set-Location SiteServerCode:  
57:       $WQL = 'select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client from SMS_R_System inner join SMS_G_System_SYSTEM_ENCLOSURE on SMS_G_System_SYSTEM_ENCLOSURE.ResourceId = SMS_R_System.ResourceId where SMS_G_System_SYSTEM_ENCLOSURE.ChassisTypes = "8" or SMS_G_System_SYSTEM_ENCLOSURE.ChassisTypes = "9" or SMS_G_System_SYSTEM_ENCLOSURE.ChassisTypes = "10" or SMS_G_System_SYSTEM_ENCLOSURE.ChassisTypes = "14"'  
58:       $Systems = Get-WmiObject -Namespace Root\SMS\Site_BNA -Query $WQL  
59:       Foreach ($Item in $Systems) {  
60:            $QuerySystems = $QuerySystems + $Item.Name  
61:       }  
62:       Set-Location c:  
63:       $QuerySystems = $QuerySystems | Sort-Object  
64:    Return $QuerySystems  
65:         
66:       #Cleanup Local Variables  
67:       Remove-Variable -Name Item -Scope Local -Force  
68:       Remove-Variable -Name QuerySystems -Scope Local -Force  
69:       Remove-Variable -Name Systems -Scope Local -Force  
70:       Remove-Variable -Name WQL -Scope Local -Force  
71:  }  
72:    
73:  Function Get-BitlockeredSystems {  
74:    #Declare Local Variables  
75:    Set-Variable -Name BitLockerObjects -Scope Local -Force  
76:    Set-Variable -Name System -Scope Local -Force  
77:    Set-Variable -Name Systems -Scope Local -Force  
78:    
79:    $Usernames = @()  
80:    $Systems = @()  
81:    $BitLockerObjects = Get-ADObject -Filter { objectclass -eq 'msFVE-RecoveryInformation' }  
82:    foreach ($System in $BitLockerObjects) {  
83:      $System = $System.DistinguishedName  
84:      $System = $System.Split(',')  
85:      $System = $System[1]  
86:      $System = $System.Split('=')  
87:      $Systems = $Systems + $System[1]  
88:    }  
89:    Return $Systems  
90:    
91:    #Cleanup Local Variables  
92:    Remove-Variable -Name BitLockerObjects -Scope Local -Force  
93:    Remove-Variable -Name System -Scope Local -Force  
94:    Remove-Variable -Name Systems -Scope Local -Force  
95:  }  
96:    
97:  Function Confirm-Bitlockered {  
98:       param ([String[]]$Laptops, [String[]]$BitlockeredSystems)  
99:    
100:    #Declare Local Variables  
101:    Set-Variable -Name Bitlockered -Scope Local -Force  
102:    Set-Variable -Name HeaderRow -Scope Local -Force  
103:    Set-Variable -Name Laptop -Scope Local -Force  
104:    Set-Variable -Name System -Scope Local -Force  
105:         
106:       foreach ($Laptop in $Laptops) {  
107:      $Bitlockered = $false  
108:      foreach ($System in $BitlockeredSystems) {  
109:        If ($Laptop -eq $System) {  
110:          $Bitlockered = $true  
111:        }  
112:      }  
113:      If ($Bitlockered -eq $false) {  
114:        If ((Test-Path $OutputFile) -eq $false) {  
115:          $HeaderRow = "Computers"+[char]44+"Encrypted"+[char]44+"Recovery Key"  
116:          Out-File -FilePath $OutputFile -InputObject $HeaderRow -Force -Encoding UTF8  
117:        }  
118:        Out-File -FilePath $OutputFile -InputObject $Laptop -Append -Force -Encoding UTF8  
119:        Write-Host $Laptop  
120:      }  
121:       }  
122:    
123:    #Cleanup Local Variables  
124:    Remove-Variable -Name Bitlockered -Scope Local -Force  
125:    Remove-Variable -Name HeaderRow -Scope Local -Force  
126:    Remove-Variable -Name Laptop -Scope Local -Force  
127:    Remove-Variable -Name System -Scope Local -Force  
128:  }  
129:    
130:  #Declare Local Variables  
131:  Set-Variable -Name BitlockeredSystems -Scope Local -Force  
132:  Set-Variable -Name Laptops -Scope Local -Force  
133:    
134:  cls  
135:  Import-Module ActiveDirectory -Scope Global -Force  
136:  Import-Module "D:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force -Scope Global  
137:  $OutputFile = $Path + "\" + $OutputFile  
138:  ProcessTextFile  
139:  $Laptops = Get-Laptops  
140:  $BitlockeredSystems = Get-BitlockeredSystems  
141:  Confirm-Bitlockered -Laptops $Laptops -BitlockeredSystems $BitlockeredSystems  
142:    
143:  #Cleanup Local Variables  
144:  Remove-Variable -Name BitlockeredSystems -Scope Local -Force  
145:  Remove-Variable -Name Laptops -Scope Local -Force  
146: