21 June 2016

PowerShell: Automatically Populate the Microsoft Office Updates folder via SCCM

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 with the help of Sapien's PowerShell Studio that made this a breeze to write and properly code. 

The script can be executed either by a scheduled task or using Microsoft Orchestrator, which is what I am using. I also set all of my parameter values within the script, but if you do not feel comfortable doing that, you can also specify the parameter values at the command line.

The script checks the folder where the automatic deployment rule in SCCM writes the new updates to. If there are new updates since the last time it was checked, it will copy them over and email the appropriate IT staff so they know when new updates are being applied in their environment. It also has an exclusion file that allows you to exclude updates if they interfere in your environment. It keeps track of what updates have been copied by writing the copied updates to a text file. The parameters should be the only thing you need to change to adapt the script to your environment. I have put a couple of examples inside the script as how to call the script from command line, especially if you want the logs emailed.

The script can be downloaded from here.

NOTE: Last December, I wrote a version of this script to automatically inject updates into the Microsoft Office folder. I found the updates were hit and miss as to what got installed. The problem came from the fact that when the .MSP files were extracted, they would be named the same as previous versions, therefor overwriting other updates. I then thought that I would need to rename those files to a unique name so they would all get applied. I continued to rewrite the script. In talking online about it, a user pointed me to the fact that he wrote a script that did the same thing. I liked his script, but I also wanted additional features, such as email and reporting. 

NOTE: Also, Microsoft uses the same KB number in the metadata field for each patch within a service pack. Because of that, make sure to extract a service pack to the updates folder manually and don't rename them. I would add the KB number for the office service pack to the exclusion list. Here is a screenshot of how it should look for Office 2010 with the SP2 applied to the updates folder.


Here is a demo of the script being executed manually:



1:  <#  
2:       .SYNOPSIS  
3:            Microsoft Office Updater  
4:         
5:       .DESCRIPTION  
6:            This script will keep the Updates folder populated with the latest Office  
7:            updates that SCCM has downloaded. It should be setup to execute as a  
8:            scheduled task on the SCCM server. I suggest executing it once a week.  
9:         
10:       .PARAMETER Country  
11:            The language-country of the updates  
12:         
13:       .PARAMETER EmailLogs  
14:            True or false on whether to email logs of the latest updates applied to the Office Updates folder.  
15:         
16:       .PARAMETER EmailRecipients  
17:            Email address to send report to  
18:         
19:       .PARAMETER EmailSender  
20:            Sender email address  
21:         
22:       .PARAMETER ExclusionFileName  
23:            Text file containing a list of updates to exclude  
24:         
25:       .PARAMETER LogFileName  
26:            Name of the log file to be written to  
27:         
28:       .PARAMETER ProcessedUpdatesFile  
29:            Name of the file containing a list of Microsoft Office updates that have already been copied over  
30:         
31:       .PARAMETER SMTPServer  
32:            fully qualified SMTP server name  
33:         
34:       .PARAMETER SourceFolder  
35:            The folder where SCCM stores the updates  
36:         
37:       .PARAMETER UpdatesFolder  
38:            The folder where Microsoft Office looks for the updates. This is typically <Office installer folder>\Updates.  
39:         
40:       .EXAMPLE  
41:            All parameters pre-populated  
42:            powershell.exe -executionpolicy bypass -file OfficeUpdater.ps1  
43:              
44:            All parameters pre-populated without email send (-command must be used when populating boolean values at the command line)  
45:            powershell.exe -executionpolicy bypass -command OfficeUpdater.ps1 -EmailLogs $false  
46:         
47:       .NOTES  
48:            ===========================================================================  
49:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
50:            Created on:       6/15/2016 10:29 AM  
51:            Created by:       Mick Pletcher  
52:            Organization:  
53:            Filename:         OfficeUpdater.ps1  
54:            ===========================================================================  
55:  #>  
56:  [CmdletBinding()]  
57:  param  
58:  (  
59:            [ValidateNotNullOrEmpty()][string]$Country,  
60:            [ValidateNotNullOrEmpty()][boolean]$EmailLogs,  
61:            [string]$EmailRecipients,  
62:            [string]$EmailSender,  
63:            [ValidateNotNullOrEmpty()][string]$ExclusionFileName,  
64:            [ValidateNotNullOrEmpty()][string]$LogFileName,  
65:            [ValidateNotNullOrEmpty()][string]$ProcessedUpdatesFile,  
66:            [string]$SMTPServer,  
67:            [ValidateNotNullOrEmpty()][string]$SourceFolder,  
68:            [ValidateNotNullOrEmpty()][string]$UpdatesFolder  
69:  )  
70:    
71:  function Copy-Updates {  
72:  <#  
73:       .SYNOPSIS  
74:            Copy the Office updates to the updates folder  
75:         
76:       .DESCRIPTION  
77:            This will extract the .msp file from the .cab and then copy it to the Microsoft Office installation updates folder.  
78:         
79:       .PARAMETER UnprocessedUpdates  
80:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
81:         
82:       .NOTES  
83:            Additional information about the function.  
84:  #>  
85:         
86:       [CmdletBinding()]  
87:       param  
88:       (  
89:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
90:       )  
91:         
92:       $RelativePath = Get-RelativePath  
93:       $LogFile = $RelativePath + $LogFileName  
94:       $ExclusionList = Get-ExclusionList  
95:       foreach ($Update in $UnprocessedUpdates) {  
96:            $ExtractedFolder = $SourceFolder + '\' + $Update.Name + '\extracted'  
97:            If ((Test-Path $ExtractedFolder) -eq $true) {  
98:                 $Files = Get-ChildItem -Path $ExtractedFolder  
99:                 foreach ($File in $Files) {  
100:                      if ($File.Extension -eq '.msp') {  
101:                           [string]$KBUpdate = Get-MSPFileInfo -Path $File.Fullname -Property 'KBArticle Number'  
102:                           $KBUpdate = 'KB' + $KBUpdate.Trim()  
103:                           $KBUpdateShortName = $KBUpdate + '.msp'  
104:                           $KBUpdateFullName = $File.DirectoryName + '\' + $KBUpdateShortName  
105:                           $DestinationFile = $UpdatesFolder + '\' + $KBUpdateShortName  
106:                           If ($KBUpdate -notin $ExclusionList) {  
107:                                Write-Host "Renaming"$File.Name"to"$KBUpdateShortName"....." -NoNewline  
108:                                $NoOutput = Copy-Item -Path $File.Fullname -Destination $KBUpdateFullName -Force  
109:                                If ((Test-Path $KBUpdateFullName) -eq $true) {  
110:                                     Write-Host "Success" -ForegroundColor Yellow  
111:                                } else {  
112:                                     Write-Host "Failed" -ForegroundColor Red  
113:                                }  
114:                                Write-Host "Copying"$KBUpdateShortName" to Office updates folder....." -NoNewline  
115:                                $NoOutput = Copy-Item -Path $KBUpdateFullName -Destination $UpdatesFolder -Force  
116:                                If ((Test-Path $DestinationFile) -eq $true) {  
117:                                     Write-Host "Success" -ForegroundColor Yellow  
118:                                     Add-Content -Path $LogFile -Value $KBUpdate -Force  
119:                                } else {  
120:                                     Write-Host "Failed" -ForegroundColor Red  
121:                                }  
122:                           } else {  
123:                                Write-Host $KBUpdate"....." -NoNewline  
124:                                Write-Host "Excluded" -ForegroundColor Green  
125:                           }  
126:                      }  
127:                 }  
128:            }  
129:            Start-Sleep -Seconds 1  
130:       }  
131:  }  
132:    
133:  function Expand-CABFiles {  
134:  <#  
135:       .SYNOPSIS  
136:            Extract the contents of the .CAB file  
137:         
138:       .DESCRIPTION  
139:            This will extract the contents of the CAB file to a temporary subfolder for all unprocessed updates. It filters the updates to expand by the country code specified in the Country parameter. It also expands all cab with none in the name.  
140:         
141:       .PARAMETER UnprocessedUpdates  
142:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
143:         
144:       .NOTES  
145:            Additional information about the function.  
146:  #>  
147:         
148:       [CmdletBinding()]  
149:       param  
150:       (  
151:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
152:       )  
153:         
154:       $Executable = $env:windir + "\System32\expand.exe"  
155:       $Country = '*' + $Country + '*'  
156:       foreach ($Update in $UnprocessedUpdates) {  
157:            $Folder = $SourceFolder + '\' + $Update.Name  
158:            $Files = Get-ChildItem -Path $Folder  
159:            foreach ($File in $Files) {  
160:                 If (($File.Name -like $Country) -or ($File.Name -like "*none*")) {  
161:                      $ExtractedDirectory = $File.DirectoryName + '\extracted'  
162:                      If ((Test-Path $ExtractedDirectory) -eq $true) {  
163:                           $NoOutput = Remove-Item $ExtractedDirectory -Recurse -Force  
164:                      }  
165:                      $NoOutput = New-Item $ExtractedDirectory -ItemType Directory  
166:                      Write-Host "Extracting"$File.Name"....." -NoNewline  
167:                      $Parameters = [char]34 + $File.FullName + [char]34 + [char]32 + [char]34 + $ExtractedDirectory + [char]34 + [char]32 + "-f:*"  
168:                      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -WindowStyle Minimized -Passthru).ExitCode  
169:                      If ($ErrCode -eq 0) {  
170:                           Write-Host "Success" -ForegroundColor Yellow  
171:                      } else {  
172:                           Write-Host "Failed" -ForegroundColor Red  
173:                      }  
174:                 }  
175:            }  
176:       }  
177:  }  
178:    
179:  function Get-ExclusionList {  
180:  <#  
181:       .SYNOPSIS  
182:            Office Updates Exclusion List  
183:         
184:       .DESCRIPTION  
185:            Contains a list of Microsoft Office updates to exclude from installation  
186:         
187:       .NOTES  
188:            Additional information about the function.  
189:  #>  
190:         
191:       [CmdletBinding()][OutputType([object])]  
192:       param ()  
193:         
194:       $RelativePath = Get-RelativePath  
195:       $ExclusionFile = $RelativePath + $ExclusionFileName  
196:       If ((Test-Path $ExclusionFile) -eq $true) {  
197:            $ExclusionList = Get-Content $ExclusionFile -Force  
198:       } else {  
199:            $NoOutput = New-Item -Path $ExclusionFile -Force  
200:            $ExclusionList = Get-Content $ExclusionFile -Force  
201:       }  
202:       Return $ExclusionList  
203:  }  
204:    
205:  function Get-ExtractedUpdatesList {  
206:  <#  
207:       .SYNOPSIS  
208:            List of updates already extracted  
209:         
210:       .DESCRIPTION  
211:            This function retrieves the list of updates that have already been extracted to the updates folder.  
212:         
213:       .NOTES  
214:            Additional information about the function.  
215:  #>  
216:         
217:       [CmdletBinding()]  
218:       param ()  
219:         
220:       $RelativePath = Get-RelativePath  
221:       $ExtractedUpdatesFile = $RelativePath + $ProcessedUpdatesFile  
222:       If ((Test-Path $ExtractedUpdatesFile) -eq $true) {  
223:            $File = Get-Content -Path $ExtractedUpdatesFile -Force  
224:            Return $File  
225:       } else {  
226:            $NoOutput = New-Item -Path $ExtractedUpdatesFile -ItemType File -Force  
227:            Return $null  
228:       }  
229:  }  
230:    
231:  function Get-MSPFileInfo {  
232:  <#  
233:       .SYNOPSIS  
234:            Extract MSP information  
235:         
236:       .DESCRIPTION  
237:            This function will extract MSP file information from the metadata table. It has been written to be able to read data from a lot of different MSP files, including Microsoft Office updates and most application patches. There are some MSP files that were not populated with the metadata table, therefor no data is obtainable.  
238:         
239:       .PARAMETER Path  
240:            Location of the MSP file  
241:         
242:       .PARAMETER Property  
243:            A pre-defined set of properties in the msi metadata table  
244:         
245:       .NOTES  
246:            Additional information about the function.  
247:  #>  
248:         
249:       param  
250:       (  
251:                 [Parameter(Mandatory = $true)][IO.FileInfo]$Path,  
252:                 [Parameter(Mandatory = $true)][ValidateSet('Classification', 'Description', 'DisplayName', 'KBArticle Number', 'ManufacturerName', 'ReleaseVersion', 'TargetProductName')][string]$Property  
253:       )  
254:         
255:       try {  
256:            $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
257:            $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($Path.FullName, 32))  
258:            $Query = "SELECT Value FROM MsiPatchMetadata WHERE Property = '$($Property)'"  
259:            $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))  
260:            $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)  
261:            $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)  
262:            $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
263:            return $Value  
264:       } catch {  
265:            Write-Output $_.Exception.Message  
266:       }  
267:  }  
268:    
269:  function Get-NewUpdatesList {  
270:  <#  
271:       .SYNOPSIS  
272:            List new updates to install  
273:         
274:       .DESCRIPTION  
275:            Get the list of updates to process  
276:         
277:       .NOTES  
278:            Additional information about the function.  
279:  #>  
280:         
281:       [CmdletBinding()][OutputType([object])]  
282:       param ()  
283:         
284:       $UnprocessedFolders = @()  
285:       $ExtractedUpdatesList = Get-ExtractedUpdatesList  
286:       $List = Get-ChildItem $SourceFolder  
287:       foreach ($Update in $List) {  
288:            If ($Update.Name -notin $ExtractedUpdatesList ) {  
289:                 $UnprocessedFolders = $UnprocessedFolders + $Update  
290:            }  
291:       }  
292:       Return $UnprocessedFolders  
293:  }  
294:    
295:  function Get-RelativePath {  
296:  <#  
297:       .SYNOPSIS  
298:            Get the relative path  
299:         
300:       .DESCRIPTION  
301:            Returns the location of the currently running PowerShell script  
302:         
303:       .NOTES  
304:            Additional information about the function.  
305:  #>  
306:         
307:       [CmdletBinding()][OutputType([string])]  
308:       param ()  
309:         
310:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
311:       Return $Path  
312:  }  
313:    
314:  function New-LogFile {  
315:  <#  
316:       .SYNOPSIS  
317:            Generate a log file  
318:         
319:       .DESCRIPTION  
320:            Generate a log file containing a list of the KB updates copied into the updates folder  
321:         
322:       .NOTES  
323:            Additional information about the function.  
324:  #>  
325:         
326:       [CmdletBinding()][OutputType([boolean])]  
327:       param ()  
328:         
329:       $RelativePath = Get-RelativePath  
330:       $LogFile = $RelativePath + $LogFileName  
331:       If ((Test-path $LogFile) -eq $true) {  
332:            Write-Host 'Deleting old log file.....' -NoNewline  
333:            $NoOutput = Remove-Item $LogFile -Force  
334:            If ((Test-path $LogFile) -eq $false) {  
335:                 Write-Host "Success" -ForegroundColor Yellow  
336:            } else {  
337:                 Write-Host "Failed" -ForegroundColor Red  
338:                 $Success = $false  
339:            }  
340:       }  
341:       If ((Test-path $LogFile) -eq $false) {  
342:            Write-Host "Creating new log file....." -NoNewline  
343:            $NoOutput = New-Item $LogFile -Force  
344:            If ((Test-path $LogFile) -eq $true) {  
345:                 Write-Host "Success" -ForegroundColor Yellow  
346:                 $Success = $true  
347:            } else {  
348:                 Write-Host "Failed" -ForegroundColor Red  
349:                 $Success = $false  
350:            }  
351:       }  
352:       Return $Success  
353:  }  
354:    
355:  function Remove-ExtractionFolders {  
356:  <#  
357:       .SYNOPSIS  
358:            Delete the extraction folders  
359:         
360:       .DESCRIPTION  
361:            Delete the extracted folders and contents of the unprocessed updates.  
362:         
363:       .PARAMETER UnprocessedUpdates  
364:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
365:         
366:       .NOTES  
367:            Additional information about the function.  
368:  #>  
369:         
370:       [CmdletBinding()]  
371:       param  
372:       (  
373:                 [ValidateNotNullOrEmpty()][object]$UnprocessedUpdates  
374:       )  
375:         
376:       foreach ($Update in $UnprocessedUpdates) {  
377:            $ExtractedFolder = $SourceFolder + '\' + $Update.Name + '\extracted'  
378:            $Deleted = $false  
379:            $Counter = 1  
380:            If ((Test-Path $ExtractedFolder) -eq $true) {  
381:                 Do {  
382:                      Try {  
383:                           Write-Host "Removing"$ExtractedFolder"....." -NoNewline  
384:                           $NoOutput = Remove-Item $ExtractedFolder -Recurse -Force -ErrorAction Stop  
385:                           If ((Test-Path $ExtractedFolder) -eq $false) {  
386:                                Write-Host "Success" -ForegroundColor Yellow  
387:                                $Deleted = $true  
388:                           } else {  
389:                                Write-Host "Failed" -ForegroundColor Red  
390:                                $Deleted = $false  
391:                           }  
392:                      } Catch {  
393:                           $Counter++  
394:                           Write-Host 'Failed. Retrying in 5 seconds' -ForegroundColor Red  
395:                           Start-Sleep -Seconds 5  
396:                           If ((Test-Path $ExtractedFolder) -eq $true) {  
397:                                $Deleted = $false  
398:                                Write-Host "Removing"$ExtractedFolder"....." -NoNewline  
399:                                $NoOutput = Remove-Item $ExtractedFolder -Recurse -Force -ErrorAction SilentlyContinue  
400:                                If ((Test-Path $ExtractedFolder) -eq $false) {  
401:                                     Write-Host "Success" -ForegroundColor Yellow  
402:                                     $Deleted = $true  
403:                                } else {  
404:                                     Write-Host "Failed" -ForegroundColor Red  
405:                                     $Deleted = $false  
406:                                }  
407:                           }  
408:                           If ($Counter = 5) {  
409:                                $Deleted = $true  
410:                           }  
411:                      }  
412:                 } while ($Deleted = $false)  
413:                 Start-Sleep -Seconds 1  
414:            }  
415:       }  
416:  }  
417:    
418:  function Send-UpdateReport {  
419:  <#  
420:       .SYNOPSIS  
421:            Send a report of new updates applied  
422:         
423:       .DESCRIPTION  
424:            Send the log file with a list of Office updates that were copied over to the Office updates folder.  
425:         
426:       .NOTES  
427:            Additional information about the function.  
428:  #>  
429:         
430:       [CmdletBinding()]  
431:       param ()  
432:         
433:       $RelativePath = Get-RelativePath  
434:       $LogFile = $RelativePath + $LogFileName  
435:       $Date = Get-Date -Format "dd-MMMM-yyyy"  
436:       $Subject = 'Microsoft Office Updates Report as of ' + $Date  
437:       $Body = 'List of Microsoft Office Updates added to the Office installation updates folder.'  
438:       $Count = 1  
439:       Do {  
440:            Try {  
441:                 Write-Host "Emailing report....." -NoNewline  
442:                 Send-MailMessage -To $EmailRecipients -From $EmailSender -Subject $Subject -Body $Body -Attachments $LogFile -SmtpServer $SMTPServer  
443:                 Write-Host "Success" -ForegroundColor Yellow  
444:                 $Exit = $true  
445:            } Catch {  
446:                 $Count++  
447:                 If ($Count -lt 4) {  
448:                      Write-Host 'Failed to send message. Retrying.....' -ForegroundColor Red  
449:                 } else {  
450:                      Write-Host 'Failed to send message' -ForegroundColor Red  
451:                      $Exit = $true  
452:                 }  
453:            }  
454:       } Until ($Exit = $true)  
455:         
456:  }  
457:    
458:  function Update-ProcessedUpdatesFile {  
459:  <#  
460:       .SYNOPSIS  
461:            Update the Processed Updates File with new updates  
462:         
463:       .DESCRIPTION  
464:            Add updates that were copied over to the processed updates file so they do not get processed again.  
465:         
466:       .PARAMETER UnprocessedUpdates  
467:            Updates that SCCM has downloaded but have not been copied to the updates folder.  
468:         
469:       .NOTES  
470:            Additional information about the function.  
471:  #>  
472:         
473:       [CmdletBinding()]  
474:       param  
475:       (  
476:                 [ValidateNotNullOrEmpty()]$UnprocessedUpdates  
477:       )  
478:         
479:       $RelativePath = Get-RelativePath  
480:       $LogFile = $RelativePath + $ProcessedUpdatesFile  
481:       foreach ($Update in $UnprocessedUpdates) {  
482:            $Success = $false  
483:            Do {  
484:                 Try {  
485:                      Write-Host 'Adding'$Update.Name'to Processed updates.....' -NoNewline  
486:                      Add-Content -Path $LogFile -Value $Update.Name -Force -ErrorAction Stop  
487:                      $Success = $true  
488:                      Write-Host "Success" -ForegroundColor Yellow  
489:                 } Catch {  
490:                      Write-Host "Failed" -ForegroundColor Red  
491:                      $Success = $false  
492:                 }  
493:            } while ($Success -eq $false)  
494:       }  
495:  }  
496:    
497:  Clear-Host  
498:  $UnprocessedFolders = Get-NewUpdatesList  
499:  If ($UnprocessedFolders -ne $null) {  
500:       $NewLog = New-LogFile  
501:       If ($NewLog -eq $true) {  
502:            Expand-CABFiles -UnprocessedUpdates $UnprocessedFolders  
503:            Copy-Updates -UnprocessedUpdates $UnprocessedFolders  
504:            Remove-ExtractionFolders -UnprocessedUpdates $UnprocessedFolders  
505:            Update-ProcessedUpdatesFile -UnprocessedUpdates $UnprocessedFolders  
506:            If ($EmailLogs -eq $true) {  
507:                 Send-UpdateReport  
508:            }  
509:       } else {  
510:            Write-Host "Failed to create log file."  
511:       }  
512:  } else {  
513:       Write-Host "No updates to process"  
514:  }  
515:    

16 June 2016

PowerShell: Pulling information from MSP files

Recently, I began making significant updates to one of my other scripts on here. In the process of asking questions about MSP files on Facebook PowerShell groups, it was clear to me that it would only take a few tweaks to make scripts that extract MSI data also be able to extract MSP data.

I first started out by pulling the code from stack overflow on how to extract data from an MSI. Obviously, that code did not work with an MSP. It was putting up the exception code message. When I debugged it, the script was catching on line 28. Since an MSP is just a patch, and it is still a database, I figured that same code could be adapted to read the data from the MSP. The key information that helped me what from Microsoft's Installer.OpenDatabase method webpage. The parameter values and what each value does was the key to figuring this out. The parameter value of 32 indicates the database is associated with an MSP file. As you can see at the end of line 28, there is a value of 32. That was originally a 0, which associated this script with an MSI, instead of an MSP. I suggest going to the Microsoft link above and reviewing the difference parameter values that can be used.

The script below was written for obtaining information from an MSP pertaining to Microsoft Office updates. From what I have seen in several MSPs I have looked at, they all differ in what tables are available. The table you want to look at for pulling data is called MsiPatchMetadata. There are some that do not have the MsiPatchMetadata table. This is defined on line 29. Property is what contains the list of available data fields. Below is a screenshot of Adobe Reader XI update MSP file viewed through ORCA. As you can see, there are two tables and several Property fields available.


In the script below, I have included the standard fields I have seen in most MSP files. Those fields are: Classification, Description, DisplayName, KBArticle Number, ManufacturerName, ReleaseVersion, and TargetProductName. Thanks to Sapien's PowerShell Studio, the script was a snap to write! I reviewed MSP files from many different applications my firm uses, and these were the most common, which also include Microsoft Office MSP updates. You may run into additional fields also which will require you to add them to the parameter validation set of the property variable.

I did not use any main parameters because I figure this function will likely be copied and used inside other scripts.

Here is a video I made that goes through and explains the process:



You can download the script from here:


1:  <#  
2:       .SYNOPSIS  
3:            Extract MSP information  
4:         
5:       .DESCRIPTION  
6:            This script will extract MSP file information from the metadata table. It has been written to be able to read data from a lot of different MSP files, including Microsoft Office updates and most application patches. There are some MSP files that were not populated with the metadata table, therefor no data is obtainable.   
7:         
8:       .NOTES  
9:            ===========================================================================  
10:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
11:            Created on:       6/15/2016 4:07 PM  
12:            Created by:       Mick Pletcher  
13:            Organization:  
14:            Filename:         MSPInfo.ps1
15:            ===========================================================================  
16:  #>  
17:    
18:    
19:  function Get-MSPFileInfo {  
20:       param  
21:       (  
22:                 [Parameter(Mandatory = $true)][IO.FileInfo]$Path,  
23:                 [Parameter(Mandatory = $true)][ValidateSet('Classification', 'Description', 'DisplayName', 'KBArticle Number', 'ManufacturerName', 'ReleaseVersion', 'TargetProductName')][string]$Property  
24:       )  
25:         
26:       try {  
27:            $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
28:            $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($Path.FullName, 32))  
29:            $Query = "SELECT Value FROM MsiPatchMetadata WHERE Property = '$($Property)'"  
30:            $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))  
31:            $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)  
32:            $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)  
33:            $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
34:            return $Value  
35:       } catch {  
36:            Write-Output $_.Exception.Message  
37:       }  
38:  }  
39:    
40:  Get-MSPFileInfo -Path "mstore-x-none.msp" -Property 'KBArticle Number'  
41:    

06 June 2016

PowerShell: Microsoft Office and Windows MAK Activation Script

Here is a script that will activate both Microsoft Office and Microsoft Windows. I wrote this script because we currently have both x86 and x64 systems, therefore the OSPP.vbs resides in different locations. I also chose to write this script so that I did not have to enter the product keys and activation in the image. If an imaging process fails, I don't want a MAK activation wasted. This script will allow you to enter a product key and/or activate the product. I setup the script so the product keys can be entered at the command line prompt to protect the exposure of the key when used in MDT or SCCM. If you wanted to hard code the keys, that can be done at the parameter fields. I have used the script on Windows 7, Windows 10, Office 2010, and Office 2016. I also wrote this script so that if any of the functions fail, it will return an error code 1 so the build will report an error in this task sequence.

I used SAPIEN's PowerShell Studio to write this script and I can attest that it made writing it a breeze! Here are some examples below of running the script at the command prompt:

  • Input the product keys and activate both office and windows
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice   -ActivateWindows
  • Input the windows product key and activate windows
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActivateWindows
  • Input the windows product key without activation
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
  • Only activate windows 
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActivateWindows
  • Input the office product key and activate office
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice
  • Input the office product key without activation
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
  • Only activate office
    • powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActiveOffice

Here is a screenshot of the script in action:



NOTE: I advise putting the script near the end of a task sequence.

You can download the script from here.


 <#  
      .SYNOPSIS  
           Online Activation for Microsoft Office  
        
      .DESCRIPTION  
           This script will perform an online activation of Microsoft Office  
        
      .PARAMETER ConsoleTitle  
           Set the title of the console  
        
      .PARAMETER OfficeProductKey  
           Microsoft Office Product Key  
        
      .PARAMETER WindowsProductKey  
           Microsoft Windows Product Key  
        
      .PARAMETER ActivateOffice  
           Activate Microsoft Office License  
        
      .PARAMETER ActivateWindows  
           Activate Microsoft Windows License  
        
      .EXAMPLE  
           You must use -command instead of -file due to boolean value parameters.  
             
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice -ActivateWindows  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActivateWindows  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -WindowsProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActivateWindows  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" -ActiveOffice  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -OfficeProductKey "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"  
           powershell.exe -executionpolicy bypass -command ActivateOfficeWindows.ps1 -ActiveOffice  
        
      .NOTES  
           ===========================================================================  
           Created with:   SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
           Created on:     6/3/2016 4:00 PM  
           Created by:     Mick Pletcher  
           Organization:  
           Filename:       ActivateOfficeWindows.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [Parameter(Mandatory = $false)][string]$ConsoleTitle = 'Microsoft Office and Windows Activation',  
      [Parameter(Mandatory = $false)][string]$OfficeProductKey,  
      [Parameter(Mandatory = $false)][string]$WindowsProductKey,  
      [switch]$ActivateOffice,  
      [switch]$ActivateWindows  
 )  
   
 function Get-OfficeSoftwareProtectionPlatform {  
 <#  
      .SYNOPSIS  
           Find the Office Software Protection Platform  
        
      .DESCRIPTION  
           Find the OSPP.vbs script to apply the office product key and invoke online activation.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $File = Get-ChildItem $env:ProgramFiles"\Microsoft Office" -Filter "OSPP.VBS" -Recurse  
      If (($File -eq $null) -or ($File -eq '')) {  
           $File = Get-ChildItem ${env:ProgramFiles(x86)}"\Microsoft Office" -Filter "OSPP.VBS" -Recurse  
      }  
      $File = $File.FullName  
      Return $File  
 }  
   
 function Get-SoftwareLicenseManager {  
 <#  
      .SYNOPSIS  
           Find the Software License Manager script  
        
      .DESCRIPTION  
           Find the slmgr.vbs script to activate Microsoft Windows  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $File = Get-ChildItem $env:windir"\system32" | Where-Object { $_.Name -eq "slmgr.vbs" }  
      $File = $File.FullName  
      Return $File  
 }  
   
 function Invoke-OfficeActivation {  
 <#  
      .SYNOPSIS  
           Activate Microsoft Office  
        
      .DESCRIPTION  
           Trigger the online Microsoft Office activation  
        
      .PARAMETER OSPP  
           OSPP.VBS  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$OSPP  
      )  
        
      $Errors = $false  
      Write-Host "Activate Microsoft Office....." -NoNewline  
      $Executable = $env:windir + "\System32\cscript.exe"  
      $Switches = [char]34 + $OSPP + [char]34 + [char]32 + "/act"  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
           $Errors = $true  
      }  
      Return $Errors  
 }  
   
 function Invoke-WindowsActivation {  
 <#  
      .SYNOPSIS  
           Activate Microsoft Windows  
        
      .DESCRIPTION  
           Trigger the online Microsoft Windows Activation  
        
      .PARAMETER SLMGR  
           SLMGR.VBS  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SLMGR  
      )  
        
      $Errors = $false  
      Write-Host "Activate Microsoft Windows....." -NoNewline  
      $Executable = $env:windir + "\System32\cscript.exe"  
      $Switches = [char]34 + $SLMGR + [char]34 + [char]32 + "-ato"  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
           $Errors = $true  
      }  
      Return $Errors  
 }  
   
 function Set-ConsoleTitle {  
 <#  
      .SYNOPSIS  
           Console Title  
        
      .DESCRIPTION  
           Sets the title of the PowerShell Console  
        
      .PARAMETER ConsoleTitle  
           Title of the PowerShell Console  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
                [Parameter(Mandatory = $true)][String]$ConsoleTitle  
      )  
        
      $host.ui.RawUI.WindowTitle = $ConsoleTitle  
 }  
   
 function Set-OfficeProductKey {  
 <#  
      .SYNOPSIS  
           Set the Office Product Key  
        
      .DESCRIPTION  
           This will install the office product key  
        
      .PARAMETER OSPP  
           OSPP.VBS  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$OSPP  
      )  
        
      $Errors = $false  
      Write-Host "Set Microsoft Office Product Key....." -NoNewline  
      $Executable = $env:windir + "\System32\cscript.exe"  
      $Switches = [char]34 + $OSPP + [char]34 + [char]32 + "/inpkey:" + $OfficeProductKey  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
           $Errors = $true  
      }  
      Return $Errors  
 }  
   
 function Set-WindowsProductKey {  
 <#  
      .SYNOPSIS  
           Set the Microsoft Windows Product Key  
        
      .DESCRIPTION  
           This will set or change the Microsoft Windows Product Key  
        
      .PARAMETER SLMGR  
           SLMGR.VBS  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SLMGR  
      )  
        
      $Errors = $false  
      Write-Host "Set Microsoft Windows Product Key....." -NoNewline  
      $Executable = $env:windir + "\System32\cscript.exe"  
      $Switches = [char]34 + $SLMGR + [char]34 + [char]32 + "/ipk" + [char]32 + $WindowsProductKey  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -WindowStyle Minimized -Passthru).ExitCode  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code"$ErrCode -ForegroundColor Red  
           $Errors = $true  
      }  
      Return $Errors  
 }  
   
 Clear-Host  
 $ErrorReport = $false  
 #Set console title  
 Set-ConsoleTitle -ConsoleTitle $ConsoleTitle  
 #Find OSPP.vbs file  
 $OSPP = Get-OfficeSoftwareProtectionPlatform  
 #Assign Microsoft Office Product Key  
 #Check if a value was passed to $OfficeProductKey  
 If (($OfficeProductKey -ne $null) -and ($OfficeProductKey -ne '')) {  
      #Check if OSPP.vbs was found  
      If (($OSPP -ne $null) -and ($OSPP -ne '')) {  
           #Assign Microsoft Office Product Key  
           $Errors = Set-OfficeProductKey -OSPP $OSPP  
           If ($ErrorReport -eq $false) {  
                $ErrorReport = $Errors  
           }  
      } else {  
           Write-Host "Office Software Protection Platform not found to set the Microsoft Office Product Key" -ForegroundColor Red  
      }  
 }  
 #Check if $ActivateOffice was selected  
 If ($ActivateOffice.IsPresent) {  
      #Check if OSPP.vbs was found  
      If (($OSPP -ne $null) -and ($OSPP -ne '')) {  
           #Activate Microsoft Office  
           $Errors = Invoke-OfficeActivation -OSPP $OSPP  
           If ($ErrorReport -eq $false) {  
                $ErrorReport = $Errors  
           }  
      } else {  
           Write-Host "Office Software Protection Platform not found to activate Microsoft Office" -ForegroundColor Red  
      }  
 }  
 #Check if a value was passed to $WindowsProductKey  
 If (($WindowsProductKey -ne $null) -and ($WindowsProductKey -ne '')) {  
      #Find SLMGR.VBS  
      $SLMGR = Get-SoftwareLicenseManager  
      #Check if SLMGR.VBS was found  
      If (($SLMGR -ne $null) -and ($SLMGR -ne '')) {  
           #Assign Windows Product Key  
           $Errors = Set-WindowsProductKey -SLMGR $SLMGR  
           If ($ErrorReport -eq $false) {  
                $ErrorReport = $Errors  
           }  
      } else {  
           Write-Host "Software licensing management tool not found to set the Microsoft Windows Product Key" -ForegroundColor Red  
      }  
 }  
 #Check if $ActivateWindows was selected  
 If ($ActivateWindows.IsPresent) {  
      #Find SLMGR.VBS  
      $SLMGR = Get-SoftwareLicenseManager  
      #Check if SLMGR.VBS was found  
      If (($SLMGR -ne $null) -and ($SLMGR -ne '')) {  
           #Activate Micosoft Windows  
           $Errors = Invoke-WindowsActivation -SLMGR $SLMGR  
           If ($ErrorReport -eq $false) {  
                $ErrorReport = $Errors  
           }  
      } else {  
           Write-Host "Software licensing management tool not found to activate Microsoft Windows" -ForegroundColor Red  
      }  
 }  
 #Exit with an error code 1 if an error was encountered  
 If ($ErrorReport -eq $true) {  
      Exit 1  
 }  

02 June 2016

PowerShell: Uninstall Windows 10 Built-In Applications with Verification

I am in the beginning stages of building a Windows 10 image for the firm I work at. One of the things that needs to be configured is the built-in apps. Obviously, we do not want all of them. You maybe wondering why you would use this over GPO or DSC. Those are fine for a blanket setting for all machines, but some apps maybe removed, but then allowed to be reinstalled on demand, which this script would be needed for.

With the help of Sapien's PowerShell Studio in building this script, I decided to make it multi-functional. It can generate a list of all built-in apps and display the formatted names to a screen. It can also output the list to a text file with the actual names of the apps needed to uninstall them. The script can uninstall a single app via command line parameters. It can also uninstall multiple app via a text file containing the application names. You can also hard code the specific uninstalls at the bottom of the script. Here are the five examples:


  • Generate a formatted list of all installed built-in apps
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true
  • Generate a list of all installed built-in apps and write the output to a log file
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true -Log $true
  • Uninstall a single built-in app by specifying its name at the command prompt. You do need to use the official name. You can get that by generating a formatted list.
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppName "Microsoft.WindowsCamera"
  • Uninstall multiple built-in apps from a list inside a text file
    • powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppsFile "AppsUninstall.txt
  • Harcode the uninstall at the bottom of this script
    • Uninstall-BuiltInApp -AppName "Microsoft.WindowsCamera"

I included in the script the ability for it to verify the application was actually installed. While running, it will give an output to the screen with the status of each app it is uninstalling. I have only executed this on Windows 10, so I am not sure if it will work on Windows 8 and Windows 8.1, as we skipped those operating systems.

You can download the script from here.


1:  <#  
2:       .SYNOPSIS  
3:            Uninstall Build-In Apps  
4:         
5:       .DESCRIPTION  
6:            This script will uninstall built-in apps in Windows 10. The script can uninstall a single app by defining it at the command line. A list of apps can be read in from a text file and iterated through for uninstallation. Finally, they can also be hardcoded into the script.  
7:         
8:       .PARAMETER AppsFile  
9:            Text file to be read in by the script which contains a list of apps to uninstall.  
10:         
11:       .PARAMETER AppName  
12:            Name of the app to uninstall. This is defined when there is only one app to uninstall.  
13:         
14:       .PARAMETER GetAppList  
15:            True or false on generating a list of Built-in apps  
16:         
17:       .PARAMETER Log  
18:            Specify true or false on whether to generate a log file in the same directory as the script containing a list of all the built-in apps by their official name  
19:         
20:       .EXAMPLE  
21:            Generate a formatted list of all installed built-in apps  
22:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true  
23:    
24:            Generate a list of all installed built-in apps and write the output to a log file  
25:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1 -GetAppList $true -Log $true  
26:    
27:            Uninstall a single built-in app by specifying its name at the command prompt. You do need to use the official name. You can get that by generating a formatted list.  
28:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppName "Microsoft.WindowsCamera"  
29:              
30:            Uninstall multiple built-in apps from a list inside a text file  
31:                 powershell.exe -executionpolicy bypass -command "UninstallBuilt-InApps.ps1" -AppsFile "AppsUninstall.txt  
32:    
33:            Harcode the uninstall at the bottom of this script  
34:                 Uninstall-BuiltInApp -AppName "Microsoft.WindowsCamera"  
35:    
36:       .NOTES  
37:            ===========================================================================  
38:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.122  
39:            Created on:       6/1/2016 3:21 PM  
40:            Created by:       Mick Pletcher  
41:            Organization:  
42:            Filename:         UninstallBuilt-InApps.ps1  
43:            ===========================================================================  
44:  #>  
45:  [CmdletBinding()]  
46:  param  
47:  (  
48:       [string]  
49:       $AppsFile = $null,  
50:       [string]  
51:       $AppName = $null,  
52:       [ValidateNotNullOrEmpty()][boolean]  
53:       $GetAppList = $false,  
54:       [ValidateNotNullOrEmpty()][boolean]  
55:       $Log = $false  
56:  )  
57:  Import-Module Appx  
58:    
59:  function Get-AppName {  
60:  <#  
61:       .SYNOPSIS  
62:            Format App name  
63:         
64:       .DESCRIPTION  
65:            This will format a built-in app name for proper display  
66:         
67:       .PARAMETER Name  
68:            Name of the application  
69:         
70:       .EXAMPLE  
71:                      PS C:\> Get-AppName -Name 'Value1'  
72:         
73:       .NOTES  
74:            Additional information about the function.  
75:  #>  
76:         
77:       [CmdletBinding()]  
78:       param  
79:       (  
80:            [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]  
81:            $Name  
82:       )  
83:         
84:       $Temp = $Name.Split('.')  
85:       For ($j = 0; $j -lt $Temp.Count; $j++) {  
86:            $Numeric = [bool]($Temp[$j] -as [double])  
87:            If ($Temp[$j] -eq 'Net') {  
88:                 $Temp[$j] = "." + $Temp[$j]  
89:            }  
90:            If ($Numeric -eq $true) {  
91:                 If ($Temp[$j + 1] -ne $null) {  
92:                      $Temp[$j] = $Temp[$j] + '.'  
93:                 }  
94:                 $FormattedName = $FormattedName + $Temp[$j]  
95:            } else {  
96:                 $FormattedName = $FormattedName + $Temp[$j] + [char]32  
97:            }  
98:       }  
99:       Return $FormattedName  
100:  }  
101:    
102:    
103:  function Get-BuiltInAppsList {  
104:  <#  
105:       .SYNOPSIS  
106:            List all Built-In Apps  
107:         
108:       .DESCRIPTION  
109:            Query for a list of all Build-In Apps  
110:         
111:       .EXAMPLE  
112:            PS C:\> Get-BuiltInAppsList  
113:         
114:       .NOTES  
115:            Additional information about the function.  
116:  #>  
117:         
118:       [CmdletBinding()]  
119:       param ()  
120:         
121:       $Apps = Get-AppxPackage  
122:       $Apps = $Apps.Name  
123:       $Apps = $Apps | Sort-Object  
124:       If ($Log -eq $true) {  
125:            $RelativePath = Get-RelativePath  
126:            $Apps | Out-File -FilePath $RelativePath"AllAppslist.txt" -Encoding UTF8  
127:       }  
128:       For ($i = 0; $i -lt $Apps.count; $i++) {  
129:            $Temp = Get-AppName -Name $Apps[$i]  
130:            $Apps[$i] = $Temp  
131:       }  
132:       $Apps  
133:  }  
134:    
135:  function Get-RelativePath {  
136:  <#  
137:       .SYNOPSIS  
138:            Get the relative path of the PowerShell script  
139:         
140:       .DESCRIPTION  
141:            Returns the path location of the PowerShell script being executed  
142:         
143:       .EXAMPLE  
144:            PS C:\> Get-RelativePath  
145:         
146:       .NOTES  
147:            Additional information about the function.  
148:  #>  
149:         
150:       [CmdletBinding()][OutputType([string])]  
151:       param ()  
152:         
153:       $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
154:       Return $RelativePath  
155:  }  
156:    
157:  function Uninstall-BuiltInApp {  
158:  <#  
159:       .SYNOPSIS  
160:            Uninstall Windows 10 Built In App  
161:         
162:       .DESCRIPTION  
163:            This will uninstall a built-in app by passing the name of the app in.  
164:         
165:       .PARAMETER AppName  
166:            Name of the App  
167:         
168:       .EXAMPLE  
169:            PS C:\> Uninstall-BuiltInApp -AppName 'Value1'  
170:         
171:       .NOTES  
172:            Additional information about the function.  
173:  #>  
174:         
175:       [CmdletBinding()]  
176:       param  
177:       (  
178:            [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]  
179:            $AppName  
180:       )  
181:         
182:       $App = Get-AppName -Name $AppName  
183:       Write-Host "Uninstalling"$App"....." -NoNewline  
184:       $Output = Get-AppxPackage $AppName  
185:       If ($Output -eq $null) {  
186:            Write-Host "Not Installed" -ForegroundColor Yellow  
187:       } else {  
188:            $Output = Get-AppxPackage $AppName | Remove-AppxPackage  
189:            $Output = Get-AppxPackage $AppName  
190:            If ($Output -eq $null) {  
191:                 Write-Host "Success" -ForegroundColor Yellow  
192:            } else {  
193:                 Write-Host "Failed" -ForegroundColor Red  
194:            }  
195:       }  
196:  }  
197:    
198:  function Uninstall-BuiltInApps {  
199:  <#  
200:       .SYNOPSIS  
201:            Uninstall Windows 10 Built In Apps  
202:         
203:       .DESCRIPTION  
204:            This will uninstall a list of built-in apps by reading the app names from a text file located in the same directory as this script.  
205:         
206:       .NOTES  
207:            Additional information about the function.  
208:  #>  
209:         
210:       [CmdletBinding()]  
211:       param ()  
212:         
213:       $RelativePath = Get-RelativePath  
214:       $AppsFile = $RelativePath + $AppsFile  
215:       $List = Get-Content -Path $AppsFile  
216:       foreach ($App in $List) {  
217:            Uninstall-BuiltInApp -AppName $App  
218:       }  
219:  }  
220:    
221:  cls  
222:  #Generate list of all Build-In apps  
223:  If ($GetAppList -eq $true) {  
224:       Get-BuiltInAppsList  
225:  }  
226:  #Uninstall a single app  
227:  If (($AppName -ne $null) -and ($AppName -ne "")) {  
228:       Uninstall-BuiltInApp -AppName $AppName  
229:  }  
230:  #Read list of apps to uninstall from text file and uninstall all on the list  
231:  If (($GetAppList -ne $null) -and ($GetAppList -ne "")) {  
232:       Uninstall-BuiltInApps  
233:  }  
234: