27 July 2016

PowerShell: Retrieving File Details

I have been writing a new script and was in need of getting the product name from the file details. Most everything I found when Googling came up finding the file version. I finally found this site that showed how to get a list of all 288 attributes. I decided to write a function that would get all of the attributes and the property fields that are populated. I found that still did not contain all of the data I needed, especially the product field. I compared what the script returned from the 288 attributes and what was available when right-clicking on a file and looking at the properties.

I have meshed the two together to give a pretty comprehensive dynamic list of available properties. For properties that have no associated value, they do not appear in the list. The available properties change by the file type and what the author inputs in them. The properties are returned from the function as an object. I know there are more available under the security and compatibility tabs, but I ran out of time while writing this function, as I need to finish up the other script first. I promise that I will be adding at least the compatibility tab to this function later on. I put the function into a PowerShell script so that you could see an examples of it executing. Sapien's PowerShell Studio made it possible to easily formulate and write this script.

Here is a picture of a typical output from the script:


Here is a video of the script in action:



You can download the script, which contains the function, from here.



1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.125  
5:        Created on:       7/26/2016 1:39 PM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:         GetFileProperties.ps1  
9:       ===========================================================================  
10:  #>  
11:    
12:  function Get-MetaData {  
13:  <#  
14:       .SYNOPSIS  
15:            Get File MetaData  
16:         
17:       .DESCRIPTION  
18:            A detailed description of the Get-MetaData function.  
19:         
20:       .PARAMETER FileName  
21:            Name of File  
22:         
23:       .EXAMPLE  
24:            PS C:\> Get-MetaData -FileName 'Value1'  
25:         
26:       .NOTES  
27:            Additional information about the function.  
28:  #>  
29:         
30:       [CmdletBinding()][OutputType([object])]  
31:       param  
32:       (  
33:                 [ValidateNotNullOrEmpty()][string]$FileName  
34:       )  
35:         
36:       $MetaDataObject = New-Object System.Object  
37:       $shell = New-Object -COMObject Shell.Application  
38:       $folder = Split-Path $FileName  
39:       $file = Split-Path $FileName -Leaf  
40:       $shellfolder = $shell.Namespace($folder)  
41:       $shellfile = $shellfolder.ParseName($file)  
42:       $MetaDataProperties = 0..287 | Foreach-Object { '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_) }  
43:       For ($i = 0; $i -le 287; $i++) {  
44:            $Property = ($MetaDataProperties[$i].split("="))[1].Trim()  
45:            $Property = (Get-Culture).TextInfo.ToTitleCase($Property).Replace(' ', '')  
46:            $Value = $shellfolder.GetDetailsOf($shellfile, $i)  
47:            If ($Property -eq 'Attributes') {  
48:                 switch ($Value) {  
49:                      'A' {  
50:                           $Value = 'Archive (A)'  
51:                      }  
52:                      'D' {  
53:                           $Value = 'Directory (D)'  
54:                      }  
55:                      'H' {  
56:                           $Value = 'Hidden (H)'  
57:                      }  
58:                      'L' {  
59:                           $Value = 'Symlink (L)'  
60:                      }  
61:                      'R' {  
62:                           $Value = 'Read-Only (R)'  
63:                      }  
64:                      'S' {  
65:                           $Value = 'System (S)'  
66:                      }  
67:                 }  
68:            }  
69:            #Do not add metadata fields which have no information  
70:            If (($Value -ne $null) -and ($Value -ne '')) {  
71:                 $MetaDataObject | Add-Member -MemberType NoteProperty -Name $Property -Value $Value  
72:            }  
73:       }  
74:       [string]$FileVersionInfo = (Get-ItemProperty $FileName).VersionInfo  
75:       $SplitInfo = $FileVersionInfo.Split([char]13)  
76:       foreach ($Item in $SplitInfo) {  
77:            $Property = $Item.Split(":").Trim()  
78:            switch ($Property[0]) {  
79:                 "InternalName" {  
80:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name InternalName -Value $Property[1]  
81:                 }  
82:                 "OriginalFileName" {  
83:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name OriginalFileName -Value $Property[1]  
84:                 }  
85:                 "Product" {  
86:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Product -Value $Property[1]  
87:                 }  
88:                 "Debug" {  
89:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Debug -Value $Property[1]  
90:                 }  
91:                 "Patched" {  
92:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Patched -Value $Property[1]  
93:                 }  
94:                 "PreRelease" {  
95:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name PreRelease -Value $Property[1]  
96:                 }  
97:                 "PrivateBuild" {  
98:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name PrivateBuild -Value $Property[1]  
99:                 }  
100:                 "SpecialBuild" {  
101:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name SpecialBuild -Value $Property[1]  
102:                 }  
103:            }  
104:       }  
105:         
106:       #Check if file is read-only  
107:       $ReadOnly = (Get-ChildItem $FileName) | Select-Object IsReadOnly  
108:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name ReadOnly -Value $ReadOnly.IsReadOnly  
109:       #Get digital file signature information  
110:       $DigitalSignature = get-authenticodesignature -filepath $FileName  
111:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateSubject -Value $DigitalSignature.SignerCertificate.Subject  
112:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateIssuer -Value $DigitalSignature.SignerCertificate.Issuer  
113:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateSerialNumber -Value $DigitalSignature.SignerCertificate.SerialNumber  
114:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateNotBefore -Value $DigitalSignature.SignerCertificate.NotBefore  
115:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateNotAfter -Value $DigitalSignature.SignerCertificate.NotAfter  
116:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateThumbprint -Value $DigitalSignature.SignerCertificate.Thumbprint  
117:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureStatus -Value $DigitalSignature.Status  
118:       Return $MetaDataObject  
119:  }  
120:    
121:  function Get-RelativePath {  
122:  <#  
123:       .SYNOPSIS  
124:            Get the relative path  
125:         
126:       .DESCRIPTION  
127:            Returns the location of the currently running PowerShell script  
128:         
129:       .NOTES  
130:            Additional information about the function.  
131:  #>  
132:         
133:       [CmdletBinding()][OutputType([string])]  
134:       param ()  
135:         
136:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
137:       Return $Path  
138:  }  
139:    
140:  Clear-Host  
141:  $RelativePath = Get-RelativePath  
142:  $File = $RelativePath + "scepinstall.exe"  
143:  $FileMetaData = Get-MetaData -FileName $File  
144:  #Example of displaying all returned values  
145:  $FileMetaData  
146:  #Example of retrieving just the file name  
147:  $FileMetaData.Name  
148:    
149:    

22 July 2016

PowerShell: MSI Function that Installs, Uninstalls, and Repairs with Verification

With the enormous help of Sapien's PowerShell Studio, I wrote this is an all encompassing MSI update to the MSI-By-Name Uninstaller I published. This function will install, uninstall, uninstall-by-application-name, and repair an MSI. It has the ability to write logs to the %TEMP% directory, or a directory of your choice. The switches are set by default to /qb- /norestart, but can be changed by using the -Switches parameter when calling the script. I have also written this so that you can either use a GUID or the MSI installer when needing to repair or uninstall an application.

The UninstallByName works by querying WMI for the installed application. You use the -DisplayName parameter to specify the name of the application. It can be a partial name or the exact name. Once it finds a matching WMI entry, it gets the GUID and then uses that to specify the uninstall. One more thing I included in the script was the ability for it to get the application name from the registry and from within the MSI database. That means you do not need to enter a display name if you do not want to. It will grab that info on its own. This is used when the script is executing and it displays that info with what task it is performing on the screen.

Once the uninstall, install, or repair has finished, the script then uses the return code and checks the system for the presence or non-presence of the application to verify it completed successfully.

I have put much documentation within the function to provide easy comprehension on how it works. I wrote this function with functions inside it so that it would be an all-in-one function to inject into scripts. There is much documentation on how to call the function under the .Example note within the function.

Here is a screenshot and a video of the function being used to install, repair, and uninstall Netdocument ndOffice.





You can download the function from here.


 function Invoke-MSI {  
 <#  
      .SYNOPSIS  
           Invoke-MSIFileName  
        
      .DESCRIPTION  
           Installs or Uninstalls an MSIFileName packaged application  
        
      .PARAMETER DisplayName  
           A description of the DisplayName parameter.  
        
      .PARAMETER LogDirectory  
           Directory where the log file is to be written to  
        
      .PARAMETER Logging  
           Designates if logging will take place. The logs are written to the temporary directory of the profile in which this PowerShell script is executed under.  
        
      .PARAMETER MSIFileName  
           name of the MSIFileName to install  
        
      .PARAMETER MSIFilePath  
           Directory where the MSIFileName file resides. If this is left blank, the relative MSIFilePath of the script will be used.  
        
      .PARAMETER Switches  
           MSIFileName switches to use during the installation  
        
      .PARAMETER GUID  
           Product code associated with the currently installed application that used an MSIFileName for installation  
        
      .PARAMETER UninstallByName  
           Uninstall the application by its Application name. The add/remove programs will be searched in the registry for a DisplayName to match the UninstallByName. It gets the associated GUID to initiate an uninstall.  
        
      .PARAMETER Install  
           Install the MSI  
        
      .PARAMETER Uninstall  
           Uninstall the MSI  
        
      .PARAMETER Repair  
           Repair the application  
        
      .EXAMPLE  
           Install application when it resides within the same directory as this script  
                Invoke-MSI -Install -MSIFileName "ndOfficeSetup.msi" -Switches "ADDLOCAL=Word,Excel,PowerPoint,Outlook,AdobeAcrobatIntegration,AdobeReaderIntegration /qb- /norestart"  
   
           Install application using a different directory  
                Invoke-MSI -Install -MSIFileName "ndOfficeSetup.msi" -MSIFilePath "\\Netdocuments\ndoffice" -Switches "ADDLOCAL=Word,Excel,PowerPoint,Outlook,AdobeAcrobatIntegration,AdobeReaderIntegration /qb- /norestart"  
        
           Repair application by its GUID  
                Invoke-MSI -Repair -GUID "{A67CA551-ADAE-4C9B-B09D-38D84044FAB8}"  
        
           Repair application by its msi when it resides in the same directory as this script  
                Invoke-MSI -Repair -MSIFileName "ndOfficeSetup.msi"  
   
           Uninstall application by name as it appears in add/remove programs without logging  
                Invoke-MSI -UninstallByName "ndOffice"  
        
           Uninstall application by name as it appears in add/remove programs with logging  
                Invoke-MSI -UninstallByName "ndOffice" -Logging  
        
           Uninstall application by GUID  
                Invoke-MSI -Uninstall -GUID "{0F3FBC9C-A8DC-4C7A-A888-730F14CC7D05}"  
        
           Uninstall application using the MSI installer file located in the same directory as this script  
                Invoke-MSI -Uninstall -MSIFileName "ndOfficeSetup.msi"  
   
      .NOTES  
      ===========================================================================  
       Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.124  
       Created on:       7/19/2016 2:05 PM  
       Created by:       Mick Pletcher  
      ===========================================================================  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
                [string]$DisplayName,  
                [switch]$Install,  
                [string]$LogDirectory,  
                [switch]$Logging,  
                [ValidateNotNullOrEmpty()][String]$MSIFileName,  
                [string]$MSIFilePath,  
                [ValidateNotNullOrEmpty()][String]$Switches = '/qb- /norestart',  
                [string]$GUID,  
                [switch]$Repair,  
                [switch]$Uninstall,  
                [switch]$UninstallByName  
      )  
        
      function Get-MSIDatabase {  
 <#  
      .SYNOPSIS  
           Retrieve Data from MSIDatabase  
        
      .DESCRIPTION  
           Query the MSI database to retrieve the specified information from the Property table  
        
      .PARAMETER Property  
           Property to retrieve  
        
      .PARAMETER MSI  
           Name of the MSI installer  
        
      .PARAMETER Path  
           Directory where the MSI resides  
        
      .EXAMPLE  
           PS C:\> Get-MSIDatabase  
        
      .NOTES  
           Additional information about the function.  
 #>  
             
           [CmdletBinding()][OutputType([string])]  
           param  
           (  
                     [ValidateNotNullOrEmpty()][string]$Property,  
                     [ValidateNotNullOrEmpty()][string]$MSI,  
                     [ValidateNotNullOrEmpty()][string]$Path  
           )  
             
           #Get the MSI file info  
           $MSIFile = Get-Item $Path$MSI  
           #Specify the ProductName field to retrieve from the MSI database  
           try {  
                #Load the windows installer object for viewing the MSI database  
                $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
                #Get the MSI database of the specified MSI file  
                $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($MSIFile.FullName, 0))  
                #Define the query for the ProductName withing the Property table  
                $Query = "SELECT Value FROM Property WHERE Property = '$($Property)'"  
                #Query the property table within the MSI database  
                $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))  
                $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)  
                $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)  
                #Assign the ProductName to the $DisplayName variable  
                $DataField = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
                Return $DataField  
           } catch {  
                Write-Output $_.Exception.Message  
                Exit 1  
           }  
      }  
        
      function Get-DisplayNameFromRegistry {  
 <#  
      .SYNOPSIS  
           Get Registry DisplayName  
        
      .DESCRIPTION  
           Retrieve the DisplayName of the application from the registry  
        
      .PARAMETER GUID  
           Product code associated with the currently installed application that used an MSIFileName for installation  
        
      .EXAMPLE  
           PS C:\> Get-DisplayNameFromRegistry  
        
      .NOTES  
           Additional information about the function.  
 #>  
             
           [CmdletBinding()][OutputType([string])]  
           param  
           (  
                     [ValidateNotNullOrEmpty()][string]$GUID  
           )  
             
           #Get system architecture -- 32-bit or 64-Bit  
           $OSArchitecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
           #Get the add/remove program entries from the registry  
           $Registry = Get-ChildItem Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall  
           If ($OSArchitecture.OSArchitecture -eq "64-bit") {  
                $Registry += Get-ChildItem Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall  
           }  
           #Find the add/remove program entry for the specific GUID  
           $Registry = $Registry | Where-Object { $_.PSChildName -eq $GUID }  
           #Format the Registry name for the Get-ItemProperty  
           $Registry = "Registry::" + $Registry.Name  
           #Get the registry values for the GUID registry entry  
           $Registry = Get-ItemProperty $Registry -ErrorAction SilentlyContinue  
           #Retrieve the application display name  
           $DisplayName = $Registry.DisplayName  
           Return $DisplayName  
      }  
        
      #Get the system architecture -- 32-bit or 64-bit  
      $OSArchitecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
      #Path to msiexec.exe  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      #Unless $Path is assigned a value, use the relative path of this PowerShell script where the MSI is located  
      If ($MSIFilePath -eq "") {  
           If (($GUID -eq $null) -or ($GUID -eq "")) {  
                $MSIFilePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
           }  
      } else {  
           If ($MSIFilePath[$MSIFilePath.Length - 1] -ne '\') {  
                $MSIFilePath += '\'  
           }  
      }  
      If ($Install.IsPresent) {  
           $Parameters = "/i" + [char]32 + [char]34 + $MSIFilePath + $MSIFileName + [char]34  
           $DisplayName = Get-MSIDatabase -Property "ProductName" -MSI $MSIFileName -Path $MSIFilePath  
           Write-Host "Installing"$DisplayName"....." -NoNewline  
      } elseif ($Uninstall.IsPresent) {  
           If ($GUID -ne "") {  
                $Parameters = "/x" + [char]32 + $GUID  
                $DisplayName = Get-DisplayNameFromRegistry -GUID $GUID  
           } else {  
                $Parameters = "/x" + [char]32 + [char]34 + $MSIFilePath + $MSIFileName + [char]34  
                $DisplayName = Get-MSIDatabase -Property "ProductName" -MSI $MSIFileName -Path $MSIFilePath  
           }  
           If ($DisplayName -ne "") {  
                Write-Host "Uninstalling"$DisplayName"....." -NoNewline  
           } else {  
                Write-Host "Uninstalling"$GUID"....." -NoNewline  
           }  
      } elseif ($UninstallByName.IsPresent) {  
           $Uninstaller = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse -ErrorAction SilentlyContinue  
           If ($OSArchitecture.OSArchitecture -eq "64-Bit") {  
                $Uninstaller += Get-ChildItem "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse -ErrorAction SilentlyContinue  
           }  
           $SearchName = "*" + $DisplayName + "*"  
           $IdentifyingNumber = get-wmiobject win32_product | where-object { $_.Name -like $SearchName }  
           [string]$GUID = $IdentifyingNumber.IdentifyingNumber  
           $Parameters = "/x" + [char]32 + $GUID  
           $DisplayName = Get-DisplayNameFromRegistry -GUID $GUID  
           If ($DisplayName -ne "") {  
                Write-Host "Uninstalling"$DisplayName"....." -NoNewline  
           } else {  
                Write-Host "Uninstalling"$GUID"....." -NoNewline  
           }  
      } elseif ($Repair.IsPresent) {  
           If ($GUID -ne "") {  
                $Parameters = "/faumsv" + [char]32 + $GUID  
                $DisplayName = Get-DisplayNameFromRegistry -GUID $GUID  
           } else {  
                $Parameters = "/faumsv" + [char]32 + [char]34 + $MSIFilePath + $MSIFileName + [char]34  
                $DisplayName = Get-MSIDatabase -Property "ProductName" -MSI $MSIFileName -Path $MSIFilePath  
           }  
           Write-Host "Repairing"$DisplayName"....." -NoNewline  
      } else {  
           Write-Host "Specify to install, repair, or uninstall the MSI" -ForegroundColor Red  
           Exit 1  
      }  
      #Add verbose logging to the parameters  
      If ($Logging.IsPresent) {  
           If ($LogDirectory -eq "") {  
                $Parameters += [char]32 + "/lvx " + [char]34 + $env:TEMP + "\" + $DisplayName + ".log" + [char]34  
           } else {  
                If ($LogDirectory[$LogDirectory.count - 1] -ne "\") {  
                     $LogDirectory += "\"  
                }  
                $Parameters += [char]32 + "/lvx " + [char]34 + $LogDirectory + $DisplayName + ".log" + [char]34  
           }  
      }  
      #Add Switches to MSIEXEC parameters  
      $Parameters += [char]32 + $Switches  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -WindowStyle Minimized -Wait -Passthru).ExitCode  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           If ($GUID -eq "") {  
                [string]$ProductCode = Get-MSIDatabase -Property "ProductCode" -MSI $MSIFileName -Path $MSIFilePath  
           } else {  
                [string]$ProductCode = $GUID  
           }  
           $ProductCode = $ProductCode.Trim()  
           $Registry = Get-ChildItem Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall  
           If ($OSArchitecture.OSArchitecture -eq "64-bit") {  
                $Registry += Get-ChildItem Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall  
           }  
           If (($Install.IsPresent) -or ($Repair.IsPresent)) {  
                If ($ProductCode -in $Registry.PSChildName) {  
                     Write-Host "Success" -ForegroundColor Yellow  
                } else {  
                     Write-Host "Failed" -ForegroundColor Red  
                }  
           } elseif (($Uninstall.IsPresent) -or ($UninstallByName.IsPresent)) {  
                If ($ProductCode -in $Registry.PSChildName) {  
                     Write-Host "Failed" -ForegroundColor Red  
                } else {  
                     Write-Host "Success" -ForegroundColor Yellow  
                }  
           }  
      } elseif ($ErrCode -eq 1605) {  
           Write-Host "Application already uninstalled" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
 }  
   

11 July 2016

PowerShell: Installed Applications Report

While working on the new Windows 10 build for my firm, it came up about updating the old script that generates a custom list of installed applications to a .CSV file on a weekly basis. The purpose of this report is so the build team has a concise list of optional applications they need to install during a replacement build for a user. Yes, they do have access to the SCCM Resource Manager, but the problem with that is combing through the list of a LOT of apps and trying to filter out which ones they need to install that are not included with the standard image. Our build team has really liked this script. This script filters all of the unnecessary apps out of the list. It will query a list of all installed apps from the add/remove programs entries within the registry. There is an external ExclusionList.txt file the script reads. This file contains a list of the applications you do not want to be included in the report. The application in the file need to be exactly how they appear in the report. You can copy and paste the apps from the report to the ExclusionList.txt file.

Thanks to Sapien's PowerShell Studio, I have been able to easily add some great new features to the script giving it the ability to rewrite the ExclusionList.txt file, thereby allowing it to be alphabetically sorted. It also removes copies of application names. To get around a bunch of systems writing to the same file at once, I used a Try | Catch encapsulated in a Do | While statement for writing to the file so errors will not pop up and if another system has already sorted and rewritten the ExclusionList.txt file, it will not occur again.

The script will write both to the screen and to the .CSV file. I added two parameters that allow you to define where the .CSV file is to be written and what filename you want it to be. Unlike the original script, this gives you the ease to write the logs to a network share instead of locally in the event a system failure occurs and you want the report for building a new system.

To use the script, I have it executed by an SCCM package once a week during prime business hours so that it runs on the maximum number of machines possible. The build team reports to me when new, unnecessary apps appear in the report so they can be added to the ExclusionList.txt file. If for some reason there is not a current report, such as a laptop has been offline for quite a long time, the script can be manually executed on a machine.

You can download the app from here.

InstalledApplications.ps1


1:  <#  
2:       .SYNOPSIS  
3:            Installed Applications  
4:         
5:       .DESCRIPTION  
6:            This will retrieve the list of installed applications from add/remove programs and write the list to a .CSV file. The tool is executed on machines once a week via an SCCM Application deployment. It's purpose is to provide a custom report to a build team for when they need to rebuild systems without having to comb through the typical SCCM Add/Remove Programs report. The reports are customized by excluding applications that are written to the ExclusionList.txt file.  
7:         
8:       .PARAMETER ReportFile  
9:            Name of the report file to be created. The report file should have the extension .CSV since this script writes to the file using UTF8 and in Excel format  
10:         
11:       .PARAMETER ReportFileLocation  
12:            The directory where the report file is located  
13:         
14:       .EXAMPLE  
15:            powershell.exe -executionpolicy bypass -file InstalledApplications.ps1  
16:         
17:       .NOTES  
18:            ===========================================================================  
19:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.124  
20:            Created on:       7/8/2016 1:29 AM  
21:            Created by:       Mick Pletcher  
22:            Organization:  
23:            Filename:         InstalledApplications.ps1  
24:            ===========================================================================  
25:  #>  
26:  [CmdletBinding()]  
27:  param  
28:  (  
29:            [ValidateNotNullOrEmpty()][string]$ReportFile = 'Applications.csv',  
30:            [ValidateNotNullOrEmpty()][string]$ReportFileLocation = 'c:\windows\waller'  
31:  )  
32:    
33:    
34:  function Get-AddRemovePrograms {  
35:  <#  
36:       .SYNOPSIS  
37:            Retrieve a list of the Add/Remove Programs  
38:         
39:       .DESCRIPTION  
40:            Retrieves the Add/Remove Programs list from the registry  
41:         
42:       .NOTES  
43:            Additional information about the function.  
44:  #>  
45:         
46:       [CmdletBinding()][OutputType([string])]  
47:       param ()  
48:         
49:       $Architecture = Get-Architecture  
50:       if ($Architecture -eq "32-bit") {  
51:            $Applications = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" | ForEach-Object -Process { $_.GetValue("DisplayName") }  
52:       } else {  
53:            $Applicationsx86 = Get-ChildItem -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" | ForEach-Object -Process { $_.GetValue("DisplayName") }  
54:            $Applicationsx64 = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" | ForEach-Object -Process { $_.GetValue("DisplayName") }  
55:            $Applications = $Applicationsx86 + $Applicationsx64  
56:       }  
57:       $Applications = $Applications | Sort-Object  
58:       $Applications = $Applications | Select-Object -Unique  
59:       Return $Applications  
60:  }  
61:    
62:  function Get-Architecture {  
63:  <#  
64:       .SYNOPSIS  
65:            Get-Architecture  
66:         
67:       .DESCRIPTION  
68:            Returns whether the system architecture is 32-bit or 64-bit  
69:         
70:       .EXAMPLE  
71:            Get-Architecture  
72:         
73:       .NOTES  
74:            Additional information about the function.  
75:  #>  
76:         
77:       [CmdletBinding()][OutputType([string])]  
78:       param ()  
79:         
80:       $OSArchitecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
81:       $OSArchitecture = $OSArchitecture.OSArchitecture  
82:       Return $OSArchitecture  
83:  }  
84:    
85:  function Get-RelativePath {  
86:  <#  
87:       .SYNOPSIS  
88:            Get the relative path  
89:         
90:       .DESCRIPTION  
91:            Returns the location of the currently running PowerShell script  
92:         
93:       .NOTES  
94:            Additional information about the function.  
95:  #>  
96:         
97:       [CmdletBinding()][OutputType([string])]  
98:       param ()  
99:         
100:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
101:       Return $Path  
102:  }  
103:    
104:  function New-LogFile {  
105:  <#  
106:       .SYNOPSIS  
107:            Create a new log file  
108:         
109:       .DESCRIPTION  
110:            Delete the old log file if it exists and/or create a new one  
111:         
112:       .NOTES  
113:            Additional information about the function.  
114:  #>  
115:         
116:       [CmdletBinding()]  
117:       param ()  
118:         
119:       If ($ReportFileLocation[$ReportFileLocation.Count - 1] -eq '\') {  
120:            $File = $ReportFileLocation + $ReportFile  
121:       } else {  
122:            $File = $ReportFileLocation + '\' + $ReportFile  
123:       }  
124:       if ((Test-Path $File) -eq $true) {  
125:            Remove-Item -Path $File -Force | Out-Null  
126:       }  
127:       if ((Test-Path $File) -eq $false) {  
128:            New-Item -Path $File -ItemType file -Force | Out-Null  
129:       }  
130:  }  
131:    
132:  function New-Report {  
133:  <#  
134:       .SYNOPSIS  
135:            Generate a new Add/Remove programs report  
136:         
137:       .DESCRIPTION  
138:            This will generate the list of Add/Remove programs and write to the .CSV file.  
139:         
140:       .PARAMETER Applications  
141:            List of Add/Remove programs applications  
142:         
143:       .NOTES  
144:            Additional information about the function.  
145:  #>  
146:         
147:       param  
148:       (  
149:                 [ValidateNotNullOrEmpty()][object]$Applications  
150:       )  
151:         
152:       If ($ReportFileLocation[$ReportFileLocation.Count - 1] -eq '\') {  
153:            $File = $ReportFileLocation + $ReportFile  
154:       } else {  
155:            $File = $ReportFileLocation + '\' + $ReportFile  
156:       }  
157:       If ((Test-Path $File) -eq $true) {  
158:            $Applications  
159:            Out-File -FilePath $File -InputObject $Applications -Append -Force -Encoding UTF8  
160:       } else {  
161:            Write-Host "Report File not present to generate report" -ForegroundColor Red  
162:       }  
163:  }  
164:    
165:  function Update-AppList {  
166:  <#  
167:       .SYNOPSIS  
168:            Generate updated list of Apps  
169:         
170:       .DESCRIPTION  
171:            Generate an updated list of apps by removing the apps listed in the exclusions.txt file. This function also sorts and rewrites the exclusion list back to the exclusion.txt file in the event new exclusions have been added.  
172:         
173:       .PARAMETER Applications  
174:            List of Add/Remove programs applications  
175:         
176:       .EXAMPLE  
177:            PS C:\> Update-AppList  
178:         
179:       .NOTES  
180:            Additional information about the function.  
181:  #>  
182:         
183:       [CmdletBinding()][OutputType([object])]  
184:       param  
185:       (  
186:                 [ValidateNotNullOrEmpty()][object]$Applications  
187:       )  
188:         
189:       $RelativePath = Get-RelativePath  
190:       $File = $RelativePath + "ExclusionList.txt"  
191:       If ((Test-Path $File) -eq $true) {  
192:            $Exclusions = Get-Content -Path $File  
193:            $SortedExclusions = $Exclusions | Sort-Object  
194:            $SortedExclusions = $SortedExclusions | Select-Object -Unique  
195:            $Sorted = !(Compare-Object $Exclusions $SortedExclusions -SyncWindow 0)  
196:            If ($Sorted -eq $false) {  
197:                 Do {  
198:                      Try {  
199:                           $Exclusions = Get-Content -Path $File  
200:                           $SortedExclusions = $Exclusions | Sort-Object  
201:                           $SortedExclusions = $SortedExclusions | Select-Object -Unique  
202:                           $Sorted = !(Compare-Object $Exclusions $SortedExclusions -SyncWindow 0)  
203:                           If ($Sorted -eq $false) {  
204:                                Out-File -FilePath $File -InputObject $SortedExclusions -Force -Encoding UTF8 -ErrorAction SilentlyContinue  
205:                           }  
206:                           $Success = $true  
207:                      } Catch {  
208:                           $Success = $false  
209:                      }  
210:                 }  
211:                 while ($Success -eq $false)  
212:            }  
213:            $Applications = $Applications | Where-Object { ($_ -notin $SortedExclusions) -and ($_ -ne "") -and ($_ -ne $null) }  
214:       }  
215:       Return $Applications  
216:  }  
217:    
218:  Clear-Host  
219:  New-LogFile  
220:  $Apps = Get-AddRemovePrograms  
221:  $Apps = Update-AppList -Applications $Apps  
222:  New-Report -Applications $Apps  
223:    

07 July 2016

PowerShell: CMTrace Installer

This is an update to the last CMTrace installer I wrote for Windows 7. This installer has been completely rewritten that allows it to function both for installing and configuring CMTrace. I included all of the requirements within the same script for installing as an administrator and configuring CMTrace to be the default .log file viewer under the user context. Thanks to Sapien's PowerShell Studio, the script was a breeze to write!

The script can be executed to both install CMTrace and configure it under user context if the end user has local administrator privileges. This can be done by including both -SystemInstall and -UserConfig when executing the script. I have it configured in our build to execute the script with the -SystemInstall. When a user logs in for the first time, the script executes and configures CMTrace as the default viewer. The user configuration is not completed until the system is rebooted. This can be done by several methods. You could use SCCM to execute the script one time unpon user logon, which is what I have configured. It could also be setup to execute from the HKCU runonce key. I have also included error checking within the script. If any errors are encountered, the script will return an error code 1 thereby reporting it to MDT/SCCM. One more thing I found and have not been able to get around is that the user will still get the following message the first time they open a .log file:


It does not matter if they click Yes or No. CMTrace remains as the default viewer.


Here is a video of the script executing during a system install:



Here is a picture  of the script after it completes configuring a user profile:



Here are examples on how to execute the script:

  • Install CMTrace with administrator privileges during a build process or SCCM deployment
    • powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall
  • Configure CMTrace as default .log file viewer via user context
    • powershell.exe -executionpolicy bypass -windowstyle hidden -file Install_CMTrace.ps1 -UserConfig
  • Specify a different installation directory to place CMTrace.exe
    • powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall -InstallLocation 'C:\Temp'
  • Install CMTrace on a machine where users also have local administrator privileges
    • powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall -UserConfig


You can download the script from here.



Install_CMTrace_Win10.ps1

1:  <#  
2:       .SYNOPSIS  
3:            SystemInstall CMTrace  
4:         
5:       .DESCRIPTION  
6:            This script will SystemInstall CMTrace on a system. It will make the appropriate registry changes necessary for the association of .log files with CMTrace. The script will run an initial check on the system to see if CMTrace is already installed. This is so the script can function both for installing CMTrace from an administrator perspective and associating CMTrace with .log files from a user perspective. If so, it will then make the appropriate changes to the HKCU.  
7:         
8:       .PARAMETER SystemInstall  
9:            This puts the script into SystemInstall mode thereby installing the CMTrace.exe with no user association of .log extensions to CMTrace.exe  
10:         
11:       .PARAMETER InstallLocation  
12:            Directory where to install CMTrace.exe  
13:         
14:       .PARAMETER UserConfig  
15:            This tells the script to associate .log files with CMTrace for the current logged on user  
16:         
17:       .PARAMETER PSConsoleTitle  
18:            Title of the PowerShell Console  
19:         
20:       .EXAMPLE  
21:            Install CMTrace with administrator privileges during a build process or SCCM deployment  
22:            powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall  
23:              
24:            Install CMTrace on a machine where users also have local administrator privileges  
25:            powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall -UserConfig  
26:              
27:            Configure CMTrace as default for user(s)  
28:            powershell.exe -executionpolicy bypass -windowstyle hidden -file Install_CMTrace.ps1 -UserConfig  
29:              
30:            Specify a different installation directory to place CMTrace.exe  
31:            powershell.exe -executionpolicy bypass -file Install_CMTrace.ps1 -SystemInstall -InstallLocation 'C:\Temp'  
32:         
33:       .NOTES  
34:            ===========================================================================  
35:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.124  
36:            Created on:       7/6/2016 10:37 AM  
37:            Created by:       Mick Pletcher  
38:            Organization:  
39:            Filename:         InstallCMTrace.ps1  
40:            ===========================================================================  
41:  #>  
42:  [CmdletBinding()]  
43:  param  
44:  (  
45:            [switch]$SystemInstall,  
46:            [ValidateNotNullOrEmpty()][string]$InstallLocation = 'C:\windows\system32',  
47:            [switch]$UserConfig,  
48:            [ValidateNotNullOrEmpty()][string]$PSConsoleTitle = 'CMTrace Installation'  
49:  )  
50:    
51:  function Close-Process {  
52:  <#  
53:       .SYNOPSIS  
54:            Close Process if running  
55:         
56:       .DESCRIPTION  
57:            Check if specified process is running and close if true.  
58:         
59:       .PARAMETER ProcessName  
60:            Name of the process. Do not include the extension such as .exe.  
61:         
62:       .EXAMPLE  
63:            PS C:\> Close-Process -Process 'Value1'  
64:         
65:       .NOTES  
66:            Additional information about the function.  
67:  #>  
68:         
69:       [CmdletBinding()]  
70:       param  
71:       (  
72:                 [ValidateNotNullOrEmpty()][string]$ProcessName  
73:       )  
74:         
75:       $Process = Get-Process $ProcessName -ErrorAction SilentlyContinue  
76:       If ($Process) {  
77:            Do {  
78:                 $Count++  
79:                 Write-Host "Closing"$Process.ProcessName"....." -NoNewline  
80:                 $Process | Stop-Process -Force  
81:                 Start-Sleep -Seconds 5  
82:                 $Process = Get-Process $ProcessName -ErrorAction SilentlyContinue  
83:                 If ($Process) {  
84:                      Write-Host "Failed" -ForegroundColor Red  
85:                 } else {  
86:                      Write-Host "Success" -ForegroundColor Yellow  
87:                 }  
88:            } while (($Process) -and ($Count -lt 5))  
89:       }  
90:  }  
91:    
92:  function Set-ConsoleTitle {  
93:  <#  
94:       .SYNOPSIS  
95:            Console Title  
96:         
97:       .DESCRIPTION  
98:            Sets the title of the PowerShell Console  
99:         
100:       .PARAMETER ConsoleTitle  
101:            Title of the PowerShell Console  
102:         
103:       .NOTES  
104:            Additional information about the function.  
105:  #>  
106:         
107:       [CmdletBinding()]  
108:       param  
109:       (  
110:                 [Parameter(Mandatory = $true)][String]$ConsoleTitle  
111:       )  
112:         
113:       $host.ui.RawUI.WindowTitle = $ConsoleTitle  
114:  }  
115:    
116:  function Get-RelativePath {  
117:  <#  
118:       .SYNOPSIS  
119:            Get the relative path  
120:         
121:       .DESCRIPTION  
122:            Returns the location of the currently running PowerShell script  
123:         
124:       .NOTES  
125:            Additional information about the function.  
126:  #>  
127:         
128:       [CmdletBinding()][OutputType([string])]  
129:       param ()  
130:         
131:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
132:       Return $Path  
133:  }  
134:    
135:  function Install-CMTraceExecutable {  
136:  <#  
137:       .SYNOPSIS  
138:            Install CMTrace  
139:         
140:       .DESCRIPTION  
141:            This function will install the CMTrace to the system32 directory, thereby giving access to CMTrace to the entire operating system.  
142:         
143:       .EXAMPLE  
144:            PS C:\> Install-CMTrace  
145:         
146:       .NOTES  
147:            Additional information about the function.  
148:  #>  
149:         
150:       [CmdletBinding()]  
151:       param ()  
152:         
153:       Close-Process -ProcessName 'CMTrace'  
154:       $RelativePath = Get-RelativePath  
155:       $SourceFile = $RelativePath + 'CMTrace.exe'  
156:       Write-Host "Installing CMTrace.exe....." -NoNewline  
157:       Copy-Item -Path $SourceFile -Destination $InstallLocation -Force  
158:       If ((Test-Path $InstallLocation) -eq $true) {  
159:            Write-Host "Success" -ForegroundColor Yellow  
160:            $Success = $true  
161:       } else {  
162:            Write-Host "Failed" -ForegroundColor Red  
163:            $Success = $false  
164:       }  
165:       Return $Success  
166:  }  
167:    
168:  function Register-CMTraceToHKCR {  
169:  <#  
170:       .SYNOPSIS  
171:            Add CMTrace registry keys to the HKCR  
172:         
173:       .DESCRIPTION  
174:            This will associate CMTrace with .log files within the HKCR hive  
175:         
176:       .EXAMPLE  
177:            PS C:\> Register-CMTraceToHKCR  
178:         
179:       .NOTES  
180:            Additional information about the function.  
181:  #>  
182:         
183:       [CmdletBinding()][OutputType([boolean])]  
184:       param ()  
185:         
186:       New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null  
187:       $Success = $true  
188:       $MUICacheRegKey = 'HKCR:\Local Settings\Software\Microsoft\Windows\Shell\MuiCache'  
189:       $ApplicationCompany = $InstallLocation + '.ApplicationCompany'  
190:       $ApplicationCompanyValue = 'Microsoft Corporation'  
191:       $FriendlyName = $InstallLocation + '.FriendlyAppName'  
192:       $FriendlyNameValue = "CMTrace.exe"  
193:       $LogfileRegKey = "HKCR:\Logfile\Shell\Open\Command"  
194:       $TestKey = Get-ItemProperty $MUICacheRegKey -Name $ApplicationCompany -ErrorAction SilentlyContinue  
195:       Write-Host 'Register HKCR Application Company.....' -NoNewline  
196:       If ($TestKey.$ApplicationCompany -ne $ApplicationCompanyValue) {  
197:            New-ItemProperty -Path $MUICacheRegKey -Name $ApplicationCompany -Value $ApplicationCompanyValue -PropertyType String | Out-Null  
198:            $TestKey = Get-ItemProperty -Path $MUICacheRegKey -Name $ApplicationCompany -ErrorAction SilentlyContinue  
199:            If ($TestKey.$ApplicationCompany -eq $ApplicationCompanyValue) {  
200:                 Write-Host 'Success' -ForegroundColor Yellow  
201:            } else {  
202:                 Write-Host 'Failed' -ForegroundColor Red  
203:                 $Success = $false  
204:            }  
205:       } else {  
206:            Write-Host 'Already Registered' -ForegroundColor Yellow  
207:       }  
208:       Write-Host 'Register HKCR Friendly Application Name.....' -NoNewline  
209:       $TestKey = Get-ItemProperty $MUICacheRegKey -Name $FriendlyName -ErrorAction SilentlyContinue  
210:       If ($TestKey.$FriendlyName -ne $FriendlyNameValue) {  
211:            New-ItemProperty -Path $MUICacheRegKey -Name $FriendlyName -Value $FriendlyNameValue -PropertyType String -ErrorAction SilentlyContinue | Out-Null  
212:            $TestKey = Get-ItemProperty -Path $MUICacheRegKey -Name $FriendlyName -ErrorAction SilentlyContinue  
213:            If ($TestKey.$FriendlyName -eq $FriendlyNameValue) {  
214:                 Write-Host 'Success' -ForegroundColor Yellow  
215:            } else {  
216:                 Write-Host 'Failed' -ForegroundColor Red  
217:                 $Success = $false  
218:            }  
219:       } else {  
220:            Write-Host 'Already Registered' -ForegroundColor Yellow  
221:       }  
222:       If ((Test-Path $LogfileRegKey) -eq $true) {  
223:            Write-Host "Removing HKCR:\Logfile....." -NoNewline  
224:            Remove-Item -Path "HKCR:\Logfile" -Recurse -Force  
225:            If ((Test-Path "HKCR:\Logfile") -eq $false) {  
226:                 Write-Host "Success" -ForegroundColor Yellow  
227:            } else {  
228:                 Write-Host "Failed" -ForegroundColor Red  
229:            }  
230:       }  
231:       Write-Host 'Register HKCR Logfile Classes Root.....' -NoNewline  
232:       New-Item -Path $LogfileRegKey -Force | Out-Null  
233:       New-ItemProperty -Path $LogfileRegKey -Name '(Default)' -Value $InstallLocation -Force | Out-Null  
234:       $TestKey = Get-ItemProperty -Path $LogfileRegKey -Name '(Default)' -ErrorAction SilentlyContinue  
235:       If ($TestKey.'(Default)' -eq $InstallLocation) {  
236:            Write-Host 'Success' -ForegroundColor Yellow  
237:       } else {  
238:            Write-Host 'Failed' -ForegroundColor Red  
239:            $Success = $false  
240:       }  
241:       Return $Success  
242:  }  
243:    
244:  function Register-CMTraceToHKCU {  
245:  <#  
246:       .SYNOPSIS  
247:            Add CMTrace registry keys to the HKLM  
248:         
249:       .DESCRIPTION  
250:            This will associate CMTrace with .log files within the HKLM hive  
251:         
252:       .NOTES  
253:            Additional information about the function.  
254:  #>  
255:         
256:       [CmdletBinding()][OutputType([boolean])]  
257:       param ()  
258:         
259:       $Success = $true  
260:       #HKCU:\SOFTWARE\Classes\Log.file\Shell\Open\Command Key  
261:       $ClassesLogFileRegKey = "HKCU:\SOFTWARE\Classes\Log.file\Shell\Open\Command"  
262:       $ClassesLogFileRegKeyValue = [char]34 + $InstallLocation + [char]34 + [char]32 + [char]34 + "%1" + [char]34  
263:       If ((Test-Path "HKCU:\SOFTWARE\Classes\Log.file") -eq $true) {  
264:            Write-Host "Removing HKCU Log.File association....." -NoNewline  
265:            Remove-Item -Path "HKCU:\SOFTWARE\Classes\Log.file" -Recurse -Force  
266:            If ((Test-Path $ClassesLogFileRegKey) -eq $false) {  
267:                 Write-Host "Success" -ForegroundColor Yellow  
268:            } else {  
269:                 Write-Host "Failed" -ForegroundColor Red  
270:                 $Success = $false  
271:            }  
272:       }  
273:       Write-Host "Register HKCU Log.File association....." -NoNewline  
274:       New-Item -Path $ClassesLogFileRegKey -Force | Out-Null  
275:       New-ItemProperty -Path $ClassesLogFileRegKey -Name '(Default)' -Value $ClassesLogFileRegKeyValue -Force | Out-Null  
276:       $TestKey = Get-ItemProperty -Path $ClassesLogFileRegKey -Name '(Default)' -ErrorAction SilentlyContinue  
277:       If ($TestKey.'(Default)' -eq $ClassesLogFileRegKeyValue) {  
278:            Write-Host 'Success' -ForegroundColor Yellow  
279:       } else {  
280:            Write-Host 'Failed' -ForegroundColor Red  
281:            $Success = $false  
282:       }  
283:       #HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log  
284:       $FileExtsRegKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log"  
285:       If ((Test-Path $FileExtsRegKey) -eq $true) {  
286:            Write-Host "Removing HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log....." -NoNewline  
287:            Remove-Item -Path $FileExtsRegKey -Recurse -Force  
288:            If ((Test-Path $FileExtsRegKey) -eq $false) {  
289:                 Write-Host "Success" -ForegroundColor Yellow  
290:            } else {  
291:                 Write-Host "Failed" -ForegroundColor Red  
292:                 $Success = $false  
293:            }  
294:       }  
295:       Write-Host "Registering .log key....." -NoNewline  
296:       New-Item -Path $FileExtsRegKey -Force | Out-Null  
297:       If ((Test-Path $FileExtsRegKey) -eq $true) {  
298:            Write-Host "Success" -ForegroundColor Yellow  
299:       } else {  
300:            Write-Host "Failed" -ForegroundColor Red  
301:            $Success = $false  
302:       }  
303:       #HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\OpenWithList  
304:       $OpenWithList = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\OpenWithList"  
305:       Write-Host "Registering HKCU OpenWithList....." -NoNewline  
306:       New-Item -Path $OpenWithList -Force | Out-Null  
307:       New-ItemProperty -Path $OpenWithList -Name "a" -Value "CMTrace.exe" -PropertyType String -Force | Out-Null  
308:       New-ItemProperty -Path $OpenWithList -Name "b" -Value "NOTEPAD.EXE" -PropertyType String -Force | Out-Null  
309:       New-ItemProperty -Path $OpenWithList -Name "MRUList" -Value "ab" -PropertyType String -Force | Out-Null  
310:       $TestKeyA = Get-ItemProperty -Path $OpenWithList -Name 'a' -ErrorAction SilentlyContinue  
311:       $TestKeyB = Get-ItemProperty -Path $OpenWithList -Name 'b' -ErrorAction SilentlyContinue  
312:       $TestKeyMRUList = Get-ItemProperty -Path $OpenWithList -Name 'MRUList' -ErrorAction SilentlyContinue  
313:       If (($TestKeyA.a -eq "CMTrace.exe") -and ($TestKeyB.b -eq "NOTEPAD.EXE") -and ($TestKeyMRUList.MRUList -eq "ab")) {  
314:            Write-Host "Success" -ForegroundColor Yellow  
315:       } else {  
316:            Write-Host "Failed" -ForegroundColor Red  
317:            $Success = $false  
318:       }  
319:       #HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\OpenWithProgids  
320:       $OpenWithProgids = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\OpenWithProgids"  
321:       Write-Host "Registering HKCU OpenWithProgids....." -NoNewline  
322:       New-Item -Path $OpenWithProgids -Force | Out-Null  
323:       New-ItemProperty -Path $OpenWithProgids -Name "txtfile" -PropertyType Binary -Force | Out-Null  
324:       New-ItemProperty -Path $OpenWithProgids -Name "Log.File" -PropertyType Binary -Force | Out-Null  
325:       #HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\UserChoice  
326:       $UserChoice = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.log\UserChoice"  
327:       Write-Host "Setting CMTrace as default viewer....." -NoNewline  
328:       New-Item -Path $UserChoice -Force | Out-Null  
329:       New-ItemProperty -Path $UserChoice -Name "Progid" -Value "Applications\CMTrace.exe" -PropertyType String -Force | Out-Null  
330:       $TestKey = Get-ItemProperty -Path $UserChoice -Name "Progid"  
331:       If ($TestKey.Progid -eq "Applications\CMTrace.exe") {  
332:            Write-Host "Success" -ForegroundColor Yellow  
333:       } else {  
334:            Write-Host "Failed" -ForegroundColor Red  
335:            $Success = $false  
336:       }  
337:       Return $Success  
338:  }  
339:    
340:  function Register-CMTraceToHKLM {  
341:  <#  
342:       .SYNOPSIS  
343:            Add CMTrace registry keys to the HKLM  
344:         
345:       .DESCRIPTION  
346:            This will associate CMTrace with .log files within the HKLM hive  
347:         
348:       .NOTES  
349:            Additional information about the function.  
350:  #>  
351:         
352:       [CmdletBinding()][OutputType([boolean])]  
353:       param ()  
354:         
355:       $Success = $true  
356:       $LogFileRegKey = "HKLM:\SOFTWARE\Classes\Logfile\Shell\Open\Command"  
357:       If ((Test-Path $LogFileRegKey) -eq $true) {  
358:            Remove-Item -Path "HKLM:\SOFTWARE\Classes\Logfile" -Recurse -Force  
359:       }  
360:       Write-Host 'Register HKLM Logfile Classes Root.....' -NoNewline  
361:       New-Item -Path $LogFileRegKey -Force | Out-Null  
362:       New-ItemProperty -Path $LogFileRegKey -Name '(Default)' -Value $InstallLocation -Force | Out-Null  
363:       $TestKey = Get-ItemProperty -Path $LogFileRegKey -Name '(Default)' -ErrorAction SilentlyContinue  
364:       If ($TestKey.'(Default)' -eq $InstallLocation) {  
365:            Write-Host 'Success' -ForegroundColor Yellow  
366:       } else {  
367:            Write-Host 'Failed' -ForegroundColor Red  
368:            $Success = $false  
369:       }  
370:       Return $Success  
371:  }  
372:    
373:  function Set-CMTraceFileLocation {  
374:  <#  
375:       .SYNOPSIS  
376:            Set the CMTrace File Location  
377:         
378:       .DESCRIPTION  
379:            Set the location and filename of CMTrace.exe  
380:         
381:       .EXAMPLE  
382:            PS C:\> Set-CMTraceFileLocation  
383:         
384:       .NOTES  
385:            Additional information about the function.  
386:  #>  
387:         
388:       [CmdletBinding()][OutputType([string])]  
389:       param ()  
390:         
391:       If ($InstallLocation -notlike '*CMTrace.exe*') {  
392:            If ($InstallLocation[$InstallLocation.count - 1] -eq '\') {  
393:                 $NewLocation = $InstallLocation + 'CMTrace.exe'  
394:            } else {  
395:                 $NewLocation = $InstallLocation + '\CMTrace.exe'  
396:            }  
397:       } else {  
398:            $NewLocation = $InstallLocation  
399:       }  
400:       Return $NewLocation  
401:  }  
402:    
403:    
404:  Set-ConsoleTitle -ConsoleTitle $PSConsoleTitle  
405:  Clear-Host  
406:  $Success = $true  
407:  $InstallLocation = Set-CMTraceFileLocation  
408:  If ($SystemInstall.IsPresent) {  
409:       $Status = Install-CMTraceExecutable  
410:       If ($Status = $false) {  
411:            $Success = $false  
412:       }  
413:       $Status = Register-CMTraceToHKCR  
414:       If ($Status = $false) {  
415:            $Success = $false  
416:       }  
417:       $Status = Register-CMTraceToHKLM  
418:       If ($Status = $false) {  
419:            $Success = $false  
420:       }  
421:  }  
422:  If ($UserConfig.IsPresent) {  
423:       $Status = Register-CMTraceToHKCU  
424:       If ($Status = $false) {  
425:            $Success = $false  
426:       }  
427:  }  
428:  If ($Success -eq $false) {  
429:       Exit 1  
430:  }  
431:    

01 July 2016

PowerShell: Pin and Unpin Applications to the Start Menu and Taskbar

I am in the middle of building a new Windows 10 image and testing out all of the GPOs and applications. One of the settings we do is to add apps to the taskbar and start menu. I had written a script a couple of years ago to do this, but it was in a rush when I had just started this position. With the help of using Sapien's PowerShell studio, this script was a breeze to write. I didn't have time to really put much thought into the script. This time around, I have much more time to write the scripts.

The first thing is the old script does not work with Windows 10. I started to do a little research and found this person's script. I liked it and ended up taking some references from it, but as you have probably seen in some of my other scripts, I also like verification and adding more features. The first of the features I added is being able to generate an official list of all applications on the machine that are listed within the specified GUID. The reason for this feature is that you will need to add that application exactly like it appears in the generated list, otherwise it will not pin or unpin it. I also put a feature to allow the generated list to be exported to a csv file.

The next feature is being able to add all of the apps you want to be pinned or unpinned to a text file. The script will read the text file and process each app. One thing you will see I did was to put a unpin first within the pin functions. I did this so if there is an app already pinned and a possible change was made to the app affecting the pinned shortcut, an updated one will appear. I also put examples at the bottom on how to hardcode apps directly into the script if you do not want to use a text file.

The final feature was to use boolean variables to reflect on success or failure of each processed step. This allows the script to exit out with an error code 1, thereby flagging it as failed if implemented in SCCM/MDT.

Here is a screenshot on how to populate a the text file. As you can see in the script, I hardcoded 'Applications.txt' as the name of the file to contain the list of applications. It must be in the same directory as the powershell script. You can override the hardcoded filename by using the -AppsFile parameter. To unpin apps, change the add to remove in the list below.


NOTE: It has come to my attention that on newer versions of Windows 10, the pin to taskbar verb has been removed. The version of Windows 10 I am using, which still works even with all update patches applied, is 1511 (10586.420).

You can download the script from here.



 <#  
      .SYNOPSIS  
           A brief description of the ApplicationShortcutsWindows10.ps1 file.  
        
      .DESCRIPTION  
           This script will add shortcuts to the taskbar.  
        
      .PARAMETER AppsFile  
           Name of the text file that contains a list of the applications to be added or removed  
        
      .PARAMETER ConsoleTitle  
           ConsoleTitle assigned to the PowerShell console  
        
      .PARAMETER OutputToTextFile  
           Select if output needs to go to a text file  
        
      .PARAMETER GetApplicationList  
           Get a list of applications with the specific name needed to use or pinning and unpinning  
        
      .EXAMPLE  
           Read apps from within a text file that resides in the same directory as this script  
                powershell.exe -executionpolicy bypass -file ApplicationShortcutsWin10.ps1 -AppsFile 'Applications.txt'  
   
           Get an official list of applications with the exact names that need to be used for pinning/unpinning  
                powershell.exe -executionpolicy bypass -file ApplicationShortcutsWin10.ps1 -GetApplicationList  
   
           Get an official list of applications with the exact names that need to be used for pinning/unpinning and write to the text file ApplicationList.csv residing in the same directory as this script  
                powershell.exe -executionpolicy bypass -file ApplicationShortcutsWin10.ps1 -GetApplicationList -OutputToTextFile  
   
           Near the bottom of the script are commented out lines that give examples of how to hardcode apps inside this script  
   
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.123  
           Created on:       6/29/2016 10:33 AM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         ApplicationShortcutsWindows10.ps1  
           ===========================================================================  
 #>  
   
 [CmdletBinding()]  
 param  
 (  
           [string]$AppsFile = 'Applications.txt',  
           [ValidateNotNullOrEmpty()][string]$ConsoleTitle = 'Application Shortcuts',  
           [switch]$OutputToTextFile,  
           [switch]$GetApplicationList  
 )  
   
 function Add-AppToStartMenu {  
 <#  
      .SYNOPSIS  
           Pins an application to the start menu  
        
      .DESCRIPTION  
           Add an application to the start menu  
        
      .PARAMETER Application  
           Name of the application. This can be left blank and the function will use the file description metadata instead.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
                [Parameter(Mandatory = $true)][string]$Application  
      )  
        
      $Success = $true  
      $Status = Remove-AppFromStartMenu -Application $Application  
      If ($Status -eq $false) {  
           $Success = $false  
      }  
      Write-Host 'Pinning'$Application' to start menu.....' -NoNewline  
      ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | Where-Object{ $_.Name -eq $Application }).verbs() | Where-Object{ $_.Name.replace('&', '') -match 'Pin to Start' } | ForEach-Object{ $_.DoIt() }  
      If ($? -eq $true) {  
           Write-Host 'Success' -ForegroundColor Yellow  
      } else {  
           Write-Host 'Failed' -ForegroundColor Red  
           $Success = $false  
      }  
      Return $Success  
 }  
   
 function Add-AppToTaskbar {  
 <#  
      .SYNOPSIS  
           Pins an application to the taskbar  
        
      .DESCRIPTION  
           Add an application to the taskbar  
        
      .PARAMETER Application  
           Name of the application. This can be left blank and the function will use the file description metadata instead.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
                [Parameter(Mandatory = $true)][string]$Application  
      )  
        
      $Success = $true  
      $Status = Remove-AppFromTaskbar -Application $Application  
      If ($Status -eq $false) {  
           $Success = $false  
      }  
      Write-Host 'Pinning'$Application' to start menu.....' -NoNewline  
      ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | Where-Object{ $_.Name -eq $Application }).verbs() | Where-Object{ $_.Name.replace('&', '') -match 'Pin to taskbar' } | ForEach-Object{ $_.DoIt() }  
      If ($? -eq $true) {  
           Write-Host 'Success' -ForegroundColor Yellow  
      } else {  
           Write-Host 'Failed' -ForegroundColor Red  
           $Success = $false  
      }  
      Return $Success  
 }  
   
 function Get-ApplicationList {  
 <#  
      .SYNOPSIS  
           Get list of Applications  
        
      .DESCRIPTION  
           Get a list of available applications with the precise name to use when pinning or unpinning to the taskbar and/or start menu  
        
      .PARAMETER SaveOutput  
           Save output to a text file  
        
      .EXAMPLE  
           PS C:\> Get-ApplicationList  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
                [switch]$SaveOutput  
      )  
        
      $RelativePath = Get-RelativePath  
      $OutputFile = $RelativePath + "ApplicationList.csv"  
      $Applications = (New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items()  
      $Applications = $Applications | Sort-Object -Property name -Unique  
      If ($SaveOutput.IsPresent) {  
           If ((Test-Path -Path $OutputFile) -eq $true) {  
                Remove-Item -Path $OutputFile -Force  
           }  
           "Applications" | Out-File -FilePath $OutputFile -Encoding UTF8 -Force  
           $Applications.Name | Out-File -FilePath $OutputFile -Encoding UTF8 -Append -Force  
      }  
      $Applications.Name  
 }  
   
 function Get-Applications {  
 <#  
      .SYNOPSIS  
           Get Application List  
        
      .DESCRIPTION  
           Get the list of applications to add or remove  
        
      .EXAMPLE  
           PS C:\> Get-Applications  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([object])]  
      param ()  
        
      $RelativePath = Get-RelativePath  
      $File = $RelativePath + $AppsFile  
      $Contents = Get-Content -Path $File -Force  
      Return $Contents  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 function Invoke-PinActions {  
 <#  
      .SYNOPSIS  
           Process the application list  
        
      .DESCRIPTION  
           Add or remove applications within the text file to/from the taskbar and start menu.  
        
      .PARAMETER AppList  
           List of applications  
        
      .EXAMPLE  
           PS C:\> Invoke-PinActions -AppList 'Value1'  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
                [Parameter(Mandatory = $false)][ValidateNotNullOrEmpty()][object]$AppList  
      )  
        
      $Success = $true  
      foreach ($App in $AppList) {  
           $Entry = $App.Split(',')  
           If ($Entry[1] -eq 'startmenu') {  
                If ($Entry[2] -eq 'add') {  
                     $Status = Add-AppToStartMenu -Application $Entry[0]  
                     If ($Status -eq $false) {  
                          $Success = $false  
                     }  
                } elseif ($Entry[2] -eq 'remove') {  
                     $Status = Remove-AppFromStartMenu -Application $Entry[0]  
                     If ($Status -eq $false) {  
                          $Success = $false  
                     }  
                } else {  
                     Write-Host $Entry[0]" was entered incorrectly"  
                }  
           } elseif ($Entry[1] -eq 'taskbar') {  
                If ($Entry[2] -eq 'add') {  
                     $Status = Add-AppToTaskbar -Application $Entry[0]  
                     If ($Status -eq $false) {  
                          $Success = $false  
                     }  
                } elseif ($Entry[2] -eq 'remove') {  
                     $Status = Remove-AppFromTaskbar -Application $Entry[0]  
                     If ($Status -eq $false) {  
                          $Success = $false  
                     }  
                } else {  
                     Write-Host $Entry[0]" was entered incorrectly"  
                }  
           }  
      }  
      Return $Success  
 }  
   
 function Remove-AppFromStartMenu {  
 <#  
      .SYNOPSIS  
           Remove the pinned application from the start menu  
        
      .DESCRIPTION  
           A detailed description of the Remove-AppFromStartMenu function.  
        
      .PARAMETER Application  
           Name of the application. This can be left blank and the function will use the file description metadata instead.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
                [Parameter(Mandatory = $true)][string]$Application  
      )  
        
      $Success = $true  
      Write-Host 'Unpinning'$Application' from start menu.....' -NoNewline  
      ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | Where-Object{ $_.Name -eq $Application }).verbs() | Where-Object{ $_.Name.replace('&', '') -match 'Unpin from Start' } | ForEach-Object{ $_.DoIt() }  
      If ($? -eq $true) {  
           Write-Host 'Success' -ForegroundColor Yellow  
      } else {  
           Write-Host 'Failed' -ForegroundColor Red  
           $Success = $false  
      }  
      Return $Success  
 }  
   
 function Remove-AppFromTaskbar {  
 <#  
      .SYNOPSIS  
           Unpins an application to the taskbar  
        
      .DESCRIPTION  
           Remove the pinned application from the taskbar  
        
      .PARAMETER Application  
           Name of the application. This can be left blank and the function will use the file description metadata instead.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
                [Parameter(Mandatory = $true)][string]$Application  
      )  
        
      $Success = $true  
      Write-Host 'Unpinning'$Application' from task bar.....' -NoNewline  
      ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | Where-Object{ $_.Name -eq $Application }).verbs() | Where-Object{ $_.Name.replace('&', '') -match 'Unpin from taskbar' } | ForEach-Object{ $_.DoIt() }  
      If ($? -eq $true) {  
           Write-Host 'Success' -ForegroundColor Yellow  
      } else {  
           Write-Host 'Failed' -ForegroundColor Red  
           $Success = $false  
      }  
      Return $Success  
 }  
   
 function Set-ConsoleTitle {  
 <#  
      .SYNOPSIS  
           Console Title  
        
      .DESCRIPTION  
           Sets the title of the PowerShell Console  
        
      .PARAMETER Title  
           Title of the PowerShell Console  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
                [Parameter(Mandatory = $true)][String]$Title  
      )  
        
      $host.ui.RawUI.WindowTitle = $Title  
 }  
   
 Clear-Host  
 $Success = $true  
 Set-ConsoleTitle -Title $ConsoleTitle  
 If ($GetApplicationList.IsPresent) {  
      If ($OutputToTextFile.IsPresent) {  
           Get-ApplicationList -SaveOutput  
      } else {  
           Get-ApplicationList  
      }  
 }  
 If (($AppsFile -ne $null) -or ($AppsFile -ne "")) {  
      $ApplicationList = Get-Applications  
      $Success = Invoke-PinActions -AppList $ApplicationList  
 }  
   
 #Hardcoded applications  
 <#  
 $Success = Add-AppToStartMenu -Application 'Microsoft Outlook 2010'  
 $Success = Add-AppToTaskbar -Application 'Microsoft Outlook 2010'  
 #>  
   
 If ($Success -eq $false) {  
      Exit 1  
 }