Mick's IT Blogs

Mick's IT Blogs

Latest Updates

23 August 2016

PowerShell: Import Active Directory Extension Attributes

Posted By: Mick Pletcher - 1:13 PM















Recently, my firm has started using the AD extension attributes for custom data. I wrote a script a while back that read the data from a .CSV and imported it into a specific ExtensionAttribute. This came up again and it was brought to my attention that we may start using more of the fields. In order to automate this so that I don't have to modify the script each time, I decided to write one that gets all of its information from the .CSV file. With the help of Sapien's PowerShell Studio, this script was easy to write and easy to document.

This script will import a populated .CSV file that resides in the same directory as the script. The first line in the .CSV file contains the headers. The script reads that line to know what fields to populate in AD. You enter the exact extensionAttribute name in the header field and all data beneath it will be populated in that field for each user. Here is a screenshot of a sample .CSV file.


The first line is what tells it which extension attribute to populate the data to. It can have as many attribute lines as you need. I also added the features to remove data from an extension attribute. If a cell is left blank or CLEAR is input into the cell, the extension attribute will be cleared. If you do not want anything done to an attribute, input NO CLEAR into the cell and the script will not do anything to the attribute.

Logging has also been added to the script. If you want a log file written, then populate the -LogFile parameter. If this is left unpopulated, the script will not create a log file.

Another feature it -ProcessDelay. I added this as a precautionary feature. This puts a specified XX seconds delay between each record. This allows you to slowly implement the changes in AD without it all happening at once. Say you accidentally made an error in the .CSV file and discover it midway through. At least not all records got changed at that time. If you leave that field blank, the script will not pause between each record.

The final feature I added was for the script to go back and verify the change was actually made.

I do highly recommend that you test this script out on a test account first before using it against all of your user accounts. It works in my environment, but it is no guarantee that it will work in yours.

Here is a screenshot of the script in action.



You can download the script from here.




1:  <#  
2:       .SYNOPSIS  
3:            A brief description of the ImportADExtensions.ps1 file.  
4:         
5:       .DESCRIPTION  
6:            This script will import data from a CSV file to be written to the desired extension attributes in active directory  
7:         
8:       .PARAMETER DataFile  
9:            Name of the csv file that contains the data to import into active directory  
10:         
11:       .PARAMETER LogFile  
12:            Name of the log file to write the status of each change to  
13:         
14:       .PARAMETER ProcessDelay  
15:            This will pause the script for XX number of seconds before processing the next AD user. This is intended as a safety measure in the event that wrong data is being written to each AD profile. This allows for not all profile to be affected at once.  
16:         
17:       .EXAMPLE  
18:            Run with no logging and no delays between entry changes  
19:                 powershell.exe -executionpolicy bypass -file ImportADExtensions.ps1 -DataFile Data.csv  
20:    
21:            Run with logging and no delays between entry changes  
22:                 powershell.exe -executionpolicy bypass -file ImportADExtensions.ps1 -DataFile Data.csv -LogFile ADExtensions.log  
23:    
24:            Run with logging and 10 second delay between entry changes  
25:                 powershell.exe -executionpolicy bypass -file ImportADExtensions.ps1 -DataFile Data.csv -LogFile ADExtensions.log -ProcessDelay 10  
26:    
27:            You can also pre-populate the parameters within the Param fields inside the script.  
28:    
29:       .NOTES  
30:            ===========================================================================  
31:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.126  
32:            Created on:       7/28/2016 8:55 AM  
33:            Created by:       Mick Pletcher  
34:            Organization:  
35:            Filename:         ImportADExtensions.ps1  
36:            ===========================================================================  
37:  #>  
38:  [CmdletBinding()]  
39:  param  
40:  (  
41:       [ValidateNotNullOrEmpty()][string]  
42:       $DataFile,  
43:       [string]  
44:       $LogFile,  
45:       [int]  
46:       $ProcessDelay  
47:  )  
48:    
49:  function Get-RelativePath {  
50:  <#  
51:       .SYNOPSIS  
52:            Get the relative path  
53:         
54:       .DESCRIPTION  
55:            Returns the location of the currently running PowerShell script  
56:         
57:       .NOTES  
58:            Additional information about the function.  
59:  #>  
60:         
61:       [CmdletBinding()][OutputType([string])]  
62:       param ()  
63:         
64:       $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
65:       Return $Path  
66:  }  
67:    
68:  function Import-DataFile {  
69:  <#  
70:       .SYNOPSIS  
71:            Import data file  
72:         
73:       .DESCRIPTION  
74:            Import the data from a csv file  
75:         
76:       .EXAMPLE  
77:            PS C:\> Import-DataFile  
78:         
79:       .NOTES  
80:            Additional information about the function.  
81:  #>  
82:         
83:       [CmdletBinding()][OutputType([object])]  
84:       param ()  
85:         
86:       #Get the path this script is being executed from  
87:       $RelativePath = Get-RelativePath  
88:       #Associate the relative path with the data file to be imported  
89:       $File = $RelativePath + $DataFile  
90:       #Read the data file to a variable  
91:       $FileData = Get-Content -Path $File -Force  
92:       #Get the attribute fields  
93:       $Fields = ($FileData[0]).Split(",")  
94:       $ImportedRecords = @()  
95:       foreach ($Record in $FileData) {  
96:            If ($Record -notlike "*extensionattribute*") {  
97:                 $SplitRecord = $Record.Split(",")  
98:                 $objRecord = New-Object System.Management.Automation.PSObject  
99:                 for ($i = 0; $i -lt $Fields.Length; $i++) {  
100:                      $objRecord | Add-Member -type NoteProperty -Name $Fields[$i] -Value $SplitRecord[$i]  
101:                 }  
102:                 $ImportedRecords += $objRecord  
103:            }  
104:       }  
105:       Return $ImportedRecords  
106:  }  
107:    
108:  function New-Logfile {  
109:  <#  
110:       .SYNOPSIS  
111:            Create a new log file  
112:         
113:       .DESCRIPTION  
114:            This will create a new log file. If an old one exists, it will delete it.  
115:         
116:       .EXAMPLE  
117:                      PS C:\> New-Logfile  
118:         
119:       .NOTES  
120:            Additional information about the function.  
121:  #>  
122:         
123:       [CmdletBinding()]  
124:       param ()  
125:         
126:       $RelativePath = Get-RelativePath  
127:       $Logs = $RelativePath + $LogFile  
128:       If ((Test-Path $Logs) -eq $true) {  
129:            $Output = "Deleting old log file....."  
130:            Remove-Item -Path $Logs -Force | Out-Null  
131:            If ((Test-Path $Logs) -eq $false) {  
132:                 $Output += "Success" + "`n"  
133:            } else {  
134:                 $Output += "Failed" + "`n"  
135:            }  
136:       }  
137:       If (($LogFile -ne "") -and ($LogFile -ne $null)) {  
138:            $Output += "Creating new log file....."  
139:            New-Item -Path $Logs -ItemType File -Force | Out-Null  
140:            If ((Test-Path $Logs) -eq $true) {  
141:                 $Output += "Success"  
142:            } else {  
143:                 $Output += "Failed"  
144:            }  
145:            Write-Output $Output  
146:       }  
147:  }  
148:    
149:  function Write-ExtensionAttributes {  
150:  <#  
151:       .SYNOPSIS  
152:            Write Extension Attributes to Active Directory  
153:         
154:       .DESCRIPTION  
155:            This script will write the extension attributes to active directory. It reads the name of the object field to associate with the correct extension attribute in AD.  
156:         
157:       .PARAMETER Records  
158:            List of imported objects  
159:         
160:       .EXAMPLE  
161:            PS C:\> Write-ExtensionAttributes -Records $value1  
162:         
163:       .NOTES  
164:            Additional information about the function.  
165:  #>  
166:         
167:       [CmdletBinding()]  
168:       param  
169:       (  
170:            [ValidateNotNullOrEmpty()][object]  
171:            $Records  
172:       )  
173:         
174:       #Get all member of $Records  
175:       $Fields = $Records | Get-Member  
176:       #Filter for just the extension attribute properties  
177:       $Fields = ($Fields | Where-Object { (($_.MemberType -eq "NoteProperty") -and ($_.Name -like "*extensionattribute*")) }).name  
178:       for ($i = 0; $i -lt @($Records).Count; $i++) {  
179:            #Get all active directory properties for specified user  
180:            $User = Get-ADUser $Records[$i].Username -Properties *  
181:            $Output += "User " + ($i+1) + " of " + @($Records).Count + "`n"  
182:            $Output += "Username: " + $Records[$i].Username + "`n"  
183:            foreach ($Field in $Fields) {  
184:                 $Output += $Field + ": " + $Records[$i].$Field + "`n"  
185:                 If ((($Records[$i].$Field -eq "Clear") -or ($Records[$i].$Field -eq "") -or ($Records[$i].$Field -eq $null)) -and ($Records[$i].$Field -ne "NO CLEAR")) {  
186:                      $Output += "Clearing " + $Field + "....."  
187:                      Set-ADUser -Identity $Records[$i].Username -Clear $Field  
188:                      #Get the field that was change from active directory  
189:                      $Test = Get-ADUser $Records[$i].Username -Properties * | select $Field  
190:                      #Test if the data in the AD field matches the data from the imported file  
191:                      if ($Test.$Field -eq $null) {  
192:                           $Output += "Success" + "`n"  
193:                      } else {  
194:                           $Output += "Failed" + "`n"  
195:                      }  
196:                 } elseif ($Records[$i].$Field -ne "NO CLEAR") {  
197:                      $User.$Field = $Records[$i].$Field  
198:                      $Output += "Setting " + $Field + "....."  
199:                      #Write change to active directory  
200:                      Set-ADUser -Instance $User  
201:                      #Get the field that was change from active directory  
202:                      $Test = Get-ADUser $Records[$i].Username -Properties * | select $Field  
203:                      #Test if the data in the AD field matches the data from the imported file  
204:                      if ($Test.$Field -eq $Records[$i].$Field) {  
205:                           $Output += "Success" + "`n"  
206:                      } else {  
207:                           $Output += "Failed" + "`n"  
208:                      }  
209:                 }  
210:            }  
211:            Write-Output $Output  
212:            #If the Logfile parameter is populated, then write the output to a logfile  
213:            If (($LogFile -ne "") -and ($LogFile -ne $null)) {  
214:                 #Get the path where this script is being executed from  
215:                 $RelativePath = Get-RelativePath  
216:                 #Define the log file path  
217:                 $Logs = $RelativePath + $LogFile  
218:                 #Write the output to the log file  
219:                 Add-Content -Value $Output -Path $Logs -Encoding UTF8 -Force  
220:            }  
221:            $Output = $null  
222:            If (($ProcessDelay -ne $null) -and ($ProcessDelay -ne "")) {  
223:                 Start-Sleep -Seconds $ProcessDelay  
224:            }  
225:            cls  
226:       }  
227:  }  
228:    
229:  Import-Module -Name ActiveDirectory  
230:  #Delete old log file and create a new one  
231:  New-Logfile  
232:  #Import all records from the csv file  
233:  $Records = Import-DataFile  
234:  #Apply changes to active directory  
235:  Write-ExtensionAttributes -Records $Records  
236:    

05 August 2016

SCCM Automated Endpoint Full System Scan upon Infection with Email Notification

Posted By: Mick Pletcher - 2:17 PM














While helping to manage Microsoft Endpoint, a former colleague suggested that I setup Endpoint to automatically run a full system scan each time an infection is detected. I Googled the blog posting on it and although it is a great post, I figured it could streamlined even more by just using SCCM alone to achieve the same outcome. It is nice when you are out of the office and your backup might not have the time to keep an eye on the antivirus infections. It is also beneficial if you are in an environment where the office is closed overnight and on weekends. If an infection takes place, this can help remediate it without your presence.

This is the third edition. The first edition just initiated a full system scan upon infection. The second edition initiated a full system scan plus would send an email to the designated email address. The third edition now combines the first two and allows for them to be designated using parameters. The code has also been optimized.

I decided to use the SCCM custom application detection to query a system and see if a full system scan has been performed in the event of an infection logged in the event viewer. I first started out by writing a PowerShell script that would perform a WMI query on the SCCM server for the status of the system the application detection was being run on. The problem I ran across was that the application is being run under system credentials, which would require me to pass network credentials within the script. Instead of having to do this, I decided to query the event viewer logs on the local machine to look for the last infection date/time, which is event 1116. I also found that event 1001 and provider name Microsoft Antimalware designate are used when a system scan has been performed. Here are the steps SCCM and PowerShell go through:

  1. SCCM deploys the package to the system.
  2. The application detection queries the event viewer logs for the last 1116 ID (infection).
  3. The application detection queries the event viewer logs for the last 1001 ID and "Microsoft Antimalware" provider name.
  4. If a system 1001 ID does not exist since the last infection, the custom detection method will exit out as a failure.
  5. If the custom detection failed, the AntiVirusScanEmail.ps1 file will be executed on the machine.
  6. An email is sent that tells a scan was performed on %COMPUTERNAME% with the virus details in the body if -Email switch was specified
  7. Once the scan is complete, an application deployment evaluation cycle is initiated to update the SCCM server with the status of the system.
  8. The application detection is initiated again to confirm the scan occurred.
If you desire emails be sent alerting you of an infection and system scan, then you will need to download and place PsExec.exe in the same directory as this script. The next thing will be to define the Installation program in SCCM using psexec.exe. This allows the PowerShell script to be executed under a domain account, thereby giving it the ability to use the send-mailmessage commandlet.  Here is how to do this:

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

Do not change %computername%. The only parts of the psexec.exe command line that need to be changed are <domain>, <username>, and <password>. 

If you do not want emails sent, then you can use the following command line parameter:

powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -FullScan
or
powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -QuickScan

This is setup in SCCM as a normal application deployment. The only thing that differs from a standard deployment is the application detection method. The ApplicationVirusDetectionMethodEmail.ps1 script is imported in for the detection method. The AntiVirusScanEmail.ps1 file is setup as the installation program. I have mine entered like this:

powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -FullScan

You can see the settings in this video:




If you also want it to email you, then refer to the section above on using psexec.exe and the example to enter in as the installation program. 

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

In order for this to work in a timely manor, you will need to change the software deployment frequency under the client settings. I have mine set at every 8 hours, or three times a day.



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



ApplicationVirusDetectionMethodEmail.ps1

1:  <#       
2:       .NOTES  
3:       ===========================================================================  
4:        Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.127  
5:        Created on:       8/5/2016 11:11 AM  
6:        Created by:       Mick Pletcher  
7:        Organization:         
8:        Filename:         ApplicationVirusDetectionMethod.ps1  
9:       ===========================================================================  
10:       
11:  #>  
12:    
13:    
14:  $LastInfection = get-winevent -filterhashtable @{ logname = 'system'; ID = 1116 } -maxevents 1 -ErrorAction SilentlyContinue  
15:  $LastScan = Get-WinEvent -FilterHashtable @{ logname = 'system'; ProviderName = 'Microsoft Antimalware'; ID = 1001 } -MaxEvents 1  
16:  If ($LastScan.TimeCreated -lt $LastInfection.TimeCreated) {  
17:       #No scan since last infection  
18:       Start-Sleep -Seconds 5  
19:       exit 0  
20:  } else {  
21:       #No infection since last scan  
22:       Write-Host "No Infection"  
23:       Start-Sleep -Seconds 5  
24:       exit 0  
25:  }  
26:    


AntiVirusScanEmail.ps1

1:  <#  
2:       .SYNOPSIS  
3:            EndPoint Virus Scan  
4:         
5:       .DESCRIPTION  
6:            This script will initiate a full or quick scan, whichever switch is selected at the command line. Once the scan is completed, it will check the event viewer logs for a scan completed entry to verify the scan successfully completed. If the Email switch is designated at the command line, then an email is sent to the specified recipient. It is suggested the $EmailRecipient, $EmailSender, and $SMTPServer be predefined in the parameter field. I have also included a trigger of the application deployment evaluation cycle to expedite the process.  
7:         
8:       .PARAMETER FullScan  
9:            Initiate a full system scan  
10:         
11:       .PARAMETER QuickScan  
12:            Initiate a quick scan  
13:         
14:       .PARAMETER Email  
15:            Select if you want an email report sent to the specified email address  
16:         
17:       .PARAMETER EmailRecipient  
18:            Receipient's email address  
19:         
20:       .PARAMETER EmailSender  
21:            Sender's email address  
22:         
23:       .PARAMETER SMTPServer  
24:            SMTP server address  
25:         
26:       .EXAMPLE  
27:            Initiate a Quickscan  
28:            powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -QuickScan  
29:              
30:            Initiate a Fullscan  
31:            powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -FullScan  
32:              
33:            Initiate a Fullscan and send email report. To, From, and SMTP parameters are pre-defined  
34:            powershell.exe -executionpolicy bypass -file AntiVirusScanEmail.ps1 -FullScan -Email  
35:         
36:       .NOTES  
37:            ===========================================================================  
38:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.127  
39:            Created on:       8/5/2016 11:11 AM  
40:            Created by:       Mick Pletcher  
41:            Organization:  
42:            Filename:         AntiVirusScanEmail.ps1  
43:            ===========================================================================  
44:  #>  
45:  param  
46:  (  
47:       [switch]  
48:       $FullScan,  
49:       [switch]  
50:       $QuickScan,  
51:       [switch]  
52:       $Email,  
53:       [string]  
54:       $EmailRecipient = '',  
55:       [string]  
56:       $EmailSender = '',  
57:       [string]  
58:       $SMTPServer = ''  
59:  )  
60:    
61:  #Import the Endpoint Provider module  
62:  Import-Module $env:ProgramFiles"\Microsoft Security Client\MpProvider\MpProvider.psd1"  
63:  #Get the relative execution path of this script  
64:  $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
65:  #Find the last infection entry in the event viewer logs  
66:  $LastInfection = get-winevent -filterhashtable @{ logname = 'system'; ID = 1116 } -maxevents 1 -ErrorAction SilentlyContinue  
67:  #Full Scan  
68:  If ($FullScan.IsPresent) {  
69:       #Initiate a full system scan  
70:       Start-MProtScan -ScanType "FullScan"  
71:       #Commented area only there if you want to manually execute this script to watch it execute  
72:  <#     cls  
73:       Write-Warning "Error: $_"  
74:       Write-Host $_.Exception.ErrorCode  
75:  #>       
76:       #Get the last event viewer log written by Endpoint to check if the full system scan has finished  
77:       $LastScan = Get-WinEvent -FilterHashtable @{ logname = 'system'; ProviderName = 'Microsoft Antimalware'; ID = 1001 } -MaxEvents 1  
78:       #  
79:       If ($LastScan.Message -like '*Microsoft Antimalware scan has finished*') {  
80:            $EmailBody = "An Endpoint antimalware full system scan has been performed on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
81:       } else {  
82:            $EmailBody = "An Endpoint antimalware full system scan did not complete on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
83:       }  
84:  }  
85:  #Quick Scan  
86:  If ($QuickScan.IsPresent) {  
87:       #Initiate a quick system scan  
88:       Start-MProtScan -ScanType "QuickScan"  
89:       #Commented area only there if you want to manually execute this script to watch it execute  
90:  <#     cls  
91:       Write-Warning "Error: $_"  
92:       Write-Host $_.Exception.ErrorCode  
93:  #>       
94:       #Get the last event viewer log written by Endpoint to check if the quick system scan has finished  
95:       $LastScan = Get-WinEvent -FilterHashtable @{ logname = 'system'; ProviderName = 'Microsoft Antimalware'; ID = 1001 } -MaxEvents 1  
96:       #  
97:       If ($LastScan.Message -like '*Microsoft Antimalware scan has finished*') {  
98:            $EmailBody = "An Endpoint antimalware quick system scan has been performed on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
99:       } else {  
100:            $EmailBody = "An Endpoint antimalware quick system scan did not complete on" + [char]32 + $env:COMPUTERNAME + [char]32 + "due to the virus detection listed below." + [char]13 + [char]13 + $LastInfection.Message  
101:       }  
102:  }  
103:  #Email Infection Report  
104:  If ($Email.IsPresent) {  
105:       $Subject = "Microsoft Endpoint Infection Report"  
106:       $EmailSubject = "Virus Detection Report for" + [char]32 + $env:COMPUTERNAME  
107:       Send-MailMessage -To $EmailRecipient -From $EmailSender -Subject $Subject -Body $EmailBody -SmtpServer $SMTPServer  
108:  }  
109:  #Initiate Application Deployment Evaluation Cycle  
110:  $WMIPath = "\\" + $env:COMPUTERNAME + "\root\ccm:SMS_Client"  
111:  $SMSwmi = [wmiclass]$WMIPath  
112:  $strAction = "{00000000-0000-0000-0000-000000000121}"  
113:  [Void]$SMSwmi.TriggerSchedule($strAction)  
114:    

02 August 2016

PowerShell: SCCM Client Installer

Posted By: Mick Pletcher - 2:58 PM














It was time to upgrade the old SCCM Client Installer. I wanted to add new features and also make the script be able to execute by using parameters. With the help of Sapien's PowerShell Studio, this was very easy to do! I did not include all optional fields, but for my own company, this works great and you can easily modify it if there are additional parameters of the client installer you need in your environment.

The first thing I did was to make the parameters available for the client installer directory, client installer filename, management point, SMS site code, and two switches defining whether to install or uninstall. I centered this script around using the ccmsetup.exe file to both install and uninstall. Another feature I added is the Build parameter. This parameter can be added with the install parameter, or by itself. This configures the SCCM client in preparation for a sysprep of the computer. This is primarily done before sysprepping a machine using SCCM or MDT that captures a reference image. This allows for the client to be already present when the reference image is laid down on a newly imaged computer. It then communicates back to the SCCM server to configure itself as a new client when that reference image is laid down on new machines. I have included command line examples within the script's documentation.

This is a video of the script executing to uninstall the SCCM client on a machine.





This video shows the script installing the SCCM client. You can see that I include the uninstallation of the application first so that if this is being run to fix a machine, it will first uninstall and then install the client. Another thing you will see is that it initializes the installation and then goes on to wait for the installation to complete. I put the waiting in there because once the client begins its install, it closes out the first instance of the ccmsetup.exe and opens up a new one, thereby making the script think the installation finished, when in essence, it did not. I make the script wait because I want to make sure that when the install takes place, it is completely finished when the script closes. When the script executes during a build, especially during a reference image build, it needs to time complete the setup before the sysprep. That is the purpose of the wait. If you do not want it to wait, you can disable lines 264 and 280.


Finally, below is a pic of the script being executed to prepare the SCCM client for a sysprep. I have also included a pop-up window in the event the SCCM client is not properly prepared for the sysprep. The window will stay there until OK is clicked. This gives the admin the capability of either manually deleting the file and registry keys and stopping the ccmexec service, or restarting the build. 



You can download the script from here.



1:  <#  
2:       .SYNOPSIS  
3:            Install SCCM Client  
4:         
5:       .DESCRIPTION  
6:            Uninstall any old client and install the new SCCM client  
7:         
8:       .PARAMETER Build  
9:            Select if this is being executed while building a reference image  
10:         
11:       .PARAMETER ClientInstallationDirectory  
12:            Directory where the client ClientInstallationFile is located  
13:         
14:       .PARAMETER ClientInstallationFile  
15:            SCCM ClientInstallationFile  
16:         
17:       .PARAMETER Install  
18:            A description of the Install parameter.  
19:         
20:       .PARAMETER ManagementPoint  
21:            SCCM Management Point  
22:         
23:       .PARAMETER SMSSiteCode  
24:            SMS Site Code  
25:         
26:       .PARAMETER Uninstall  
27:            A description of the Uninstall parameter.  
28:         
29:       .PARAMETER UsePKICert  
30:            Specifies whether clients use PKI certificate when available  
31:         
32:       .PARAMETER NOCRLCheck  
33:            Specifies that clients do not check the certificate revocation list (CRL) for site systems  
34:         
35:       .PARAMETER Source  
36:            Specifies the source location from which to download installation files. This can be a local or UNC path.  
37:         
38:       .EXAMPLE  
39:            New installation  
40:            powershell.exe -executionpolicy bypass -file SCCMClient.ps1 -Install  
41:              
42:            Uninstall  
43:            powershell.exe -executionpolicy bypass -file SCCMClient.ps1 -Uninstall  
44:              
45:            SCCM/MDT/Sysprep  
46:            powershell.exe -executionpolicy bypass -file SCCMClient.ps1 -Build  
47:            powershell.exe -executionpolicy bypass -file SCCMClient.ps1 -Install -Build  
48:         
49:       .NOTES  
50:            The above examples do not include the $ClientInstallationDirectory and  
51:            the $ClientInsallationFile. I prepopulated the data within the parameter  
52:            definitions below. I also define the $ManagementPoint and $SMSSiteCode. I  
53:            have not tested the $UsePKICert, $NOCRLCheck, or $Source fields as we do  
54:            not use those where I work, therefore I cannot verify if they are valid.  
55:    
56:            ===========================================================================  
57:             Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.127  
58:             Created on:        8/2/2016 2:50 PM  
59:             Created by:        Mick Pletcher  
60:             Organization:         
61:             Filename:          SCCMClientInstaller.ps1  
62:            ===========================================================================  
63:    
64:  #>  
65:  [CmdletBinding()]  
66:  param  
67:  (  
68:       [switch]  
69:       $Build,  
70:       [ValidateNotNullOrEmpty()][string]  
71:       $ClientInstallationDirectory = '',  
72:       [ValidateNotNullOrEmpty()][string]  
73:       $ClientInstallationFile = 'ccmsetup.exe',  
74:       [switch]  
75:       $Install,  
76:       [string]  
77:       $ManagementPoint = '',  
78:       [string]  
79:       $SMSSiteCode = '',  
80:       [switch]  
81:       $Uninstall,  
82:       [switch]  
83:       $UsePKICert,  
84:       [switch]  
85:       $NOCRLCheck,  
86:       [string]  
87:       $Source  
88:  )  
89:    
90:    
91:  function Get-MetaData {  
92:  <#  
93:       .SYNOPSIS  
94:            Get File MetaData  
95:         
96:       .DESCRIPTION  
97:            A detailed description of the Get-MetaData function.  
98:         
99:       .PARAMETER FileName  
100:            Name of File  
101:         
102:       .EXAMPLE  
103:            PS C:\> Get-MetaData -FileName 'Value1'  
104:         
105:       .NOTES  
106:            Additional information about the function.  
107:  #>  
108:         
109:       [CmdletBinding()][OutputType([object])]  
110:       param  
111:       (  
112:            [ValidateNotNullOrEmpty()][string]  
113:            $FileName  
114:       )  
115:         
116:       Write-Host "Retrieving File Description Data....." -NoNewline  
117:       $MetaDataObject = New-Object System.Object  
118:       $shell = New-Object -COMObject Shell.Application  
119:       $folder = Split-Path $FileName  
120:       $file = Split-Path $FileName -Leaf  
121:       $shellfolder = $shell.Namespace($folder)  
122:       $shellfile = $shellfolder.ParseName($file)  
123:       $MetaDataProperties = 0..287 | Foreach-Object { '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_) }  
124:       For ($i = 0; $i -le 287; $i++) {  
125:            $Property = ($MetaDataProperties[$i].split("="))[1].Trim()  
126:            $Property = (Get-Culture).TextInfo.ToTitleCase($Property).Replace(' ', '')  
127:            $Value = $shellfolder.GetDetailsOf($shellfile, $i)  
128:            If ($Property -eq 'Attributes') {  
129:                 switch ($Value) {  
130:                      'A' {  
131:                           $Value = 'Archive (A)'  
132:                      }  
133:                      'D' {  
134:                           $Value = 'Directory (D)'  
135:                      }  
136:                      'H' {  
137:                           $Value = 'Hidden (H)'  
138:                      }  
139:                      'L' {  
140:                           $Value = 'Symlink (L)'  
141:                      }  
142:                      'R' {  
143:                           $Value = 'Read-Only (R)'  
144:                      }  
145:                      'S' {  
146:                           $Value = 'System (S)'  
147:                      }  
148:                 }  
149:            }  
150:            #Do not add metadata fields which have no information  
151:            If (($Value -ne $null) -and ($Value -ne '')) {  
152:                 $MetaDataObject | Add-Member -MemberType NoteProperty -Name $Property -Value $Value  
153:            }  
154:       }  
155:       [string]$FileVersionInfo = (Get-ItemProperty $FileName).VersionInfo  
156:       $SplitInfo = $FileVersionInfo.Split([char]13)  
157:       foreach ($Item in $SplitInfo) {  
158:            $Property = $Item.Split(":").Trim()  
159:            switch ($Property[0]) {  
160:                 "InternalName" {  
161:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name InternalName -Value $Property[1]  
162:                 }  
163:                 "OriginalFileName" {  
164:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name OriginalFileName -Value $Property[1]  
165:                 }  
166:                 "Product" {  
167:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Product -Value $Property[1]  
168:                 }  
169:                 "Debug" {  
170:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Debug -Value $Property[1]  
171:                 }  
172:                 "Patched" {  
173:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name Patched -Value $Property[1]  
174:                 }  
175:                 "PreRelease" {  
176:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name PreRelease -Value $Property[1]  
177:                 }  
178:                 "PrivateBuild" {  
179:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name PrivateBuild -Value $Property[1]  
180:                 }  
181:                 "SpecialBuild" {  
182:                      $MetaDataObject | Add-Member -MemberType NoteProperty -Name SpecialBuild -Value $Property[1]  
183:                 }  
184:            }  
185:       }  
186:         
187:       #Check if file is read-only  
188:       $ReadOnly = (Get-ChildItem $FileName) | Select-Object IsReadOnly  
189:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name ReadOnly -Value $ReadOnly.IsReadOnly  
190:       #Get digital file signature information  
191:       $DigitalSignature = get-authenticodesignature -filepath $FileName  
192:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateSubject -Value $DigitalSignature.SignerCertificate.Subject  
193:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateIssuer -Value $DigitalSignature.SignerCertificate.Issuer  
194:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateSerialNumber -Value $DigitalSignature.SignerCertificate.SerialNumber  
195:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateNotBefore -Value $DigitalSignature.SignerCertificate.NotBefore  
196:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateNotAfter -Value $DigitalSignature.SignerCertificate.NotAfter  
197:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureCertificateThumbprint -Value $DigitalSignature.SignerCertificate.Thumbprint  
198:       $MetaDataObject | Add-Member -MemberType NoteProperty -Name SignatureStatus -Value $DigitalSignature.Status  
199:       If (($MetaDataObject -ne "") -and ($MetaDataObject -ne $null)) {  
200:            Write-Host "Success" -ForegroundColor Yellow  
201:       } else {  
202:            Write-Host "Failed" -ForegroundColor Red  
203:       }  
204:       Return $MetaDataObject  
205:  }  
206:    
207:  function Invoke-EXE {  
208:  <#  
209:       .SYNOPSIS  
210:            Install or Uninstall Executable  
211:         
212:       .DESCRIPTION  
213:            A detailed description of the Invoke-EXE function.  
214:         
215:       .PARAMETER InstallerMetaData  
216:            The metadata extracted from the executable  
217:         
218:       .PARAMETER Install  
219:            Specify to Install the application  
220:         
221:       .PARAMETER Uninstall  
222:            Specify to uninstall the application  
223:         
224:       .PARAMETER Executable  
225:            The installation file for installing the application  
226:         
227:       .PARAMETER Switches  
228:            Switches to control the executable file  
229:         
230:       .PARAMETER DisplayName  
231:            Name to be displayed while installing or uninstalling the application  
232:         
233:       .EXAMPLE  
234:            PS C:\> Invoke-EXE  
235:         
236:       .NOTES  
237:            Additional information about the function.  
238:  #>  
239:         
240:       [CmdletBinding()][OutputType([boolean])]  
241:       param  
242:       (  
243:            [object]  
244:            $InstallerMetaData,  
245:            [switch]  
246:            $Install,  
247:            [switch]  
248:            $Uninstall,  
249:            [ValidateNotNullOrEmpty()][string]  
250:            $Executable,  
251:            [string]  
252:            $Switches,  
253:            [string]  
254:            $DisplayName  
255:       )  
256:         
257:       If ($Install.IsPresent) {  
258:            Write-Host "Initiating Installation of"$DisplayName"....." -NoNewline  
259:            $File = $env:windir + "\ccmsetup\ccmsetup.exe"  
260:            $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -WindowStyle Minimized -Wait -Passthru).ExitCode  
261:            If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
262:                 Write-Host "Success" -ForegroundColor Yellow  
263:                 If ((Test-Path $File) -eq $true) {  
264:                      Wait-ProcessEnd -ProcessName ccmsetup  
265:                 } else {  
266:                      Write-Host "Failed" -ForegroundColor Red  
267:                      $Failed = $true  
268:                 }  
269:            } else {  
270:                 Write-Host "Failed with error"$ErrCode -ForegroundColor Red  
271:            }  
272:       } elseif ($Uninstall.IsPresent) {  
273:            Write-Host "Uninstalling"$DisplayName"....." -NoNewline  
274:            $File = $env:windir + "\ccmsetup\ccmsetup.exe"  
275:            If ((Test-Path $File) -eq $true) {  
276:                 $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -WindowStyle Minimized -Wait -Passthru).ExitCode  
277:                 If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
278:                      Write-Host "Success" -ForegroundColor Yellow  
279:                      If ((Test-Path $File) -eq $true) {  
280:                           Wait-ProcessEnd -ProcessName ccmsetup  
281:                      }  
282:                 } else {  
283:                      $Failed = $true  
284:                      Write-Host "Failed with error"$ErrCode -ForegroundColor Red  
285:                 }  
286:            } else {  
287:                 Write-Host "Not Present" -ForegroundColor Green  
288:            }  
289:       }  
290:       If ($Failed -eq $true) {  
291:            Return $false  
292:       } else {  
293:            Return $true  
294:       }  
295:  }  
296:    
297:  function Remove-File {  
298:  <#  
299:       .SYNOPSIS  
300:            Delete a file with verification  
301:         
302:       .DESCRIPTION  
303:            Delete a file and verify the file no longer exists  
304:         
305:       .PARAMETER Filename  
306:            Name of the file to delete  
307:         
308:       .EXAMPLE  
309:            PS C:\> Remove-File -Filename 'Value1'  
310:         
311:       .NOTES  
312:            Additional information about the function.  
313:  #>  
314:         
315:       [CmdletBinding()][OutputType([boolean])]  
316:       param  
317:       (  
318:            [ValidateNotNullOrEmpty()][string]  
319:            $Filename  
320:       )  
321:         
322:       If ((Test-Path $Filename) -eq $false) {  
323:            Write-Host $Filename" already deleted"  
324:       } else {  
325:            $File = Get-Item $Filename -Force  
326:            Write-Host "Deleting"$File.Name"....." -NoNewline  
327:            If (Test-Path $File) {  
328:                 Remove-Item $File -Force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | Out-Null  
329:                 If ((Test-Path $Filename) -eq $False) {  
330:                      Write-Host "Success" -ForegroundColor Yellow  
331:                 } else {  
332:                      $Failed = $true  
333:                      Write-Host "Failed" -ForegroundColor Red  
334:                 }  
335:            } else {  
336:                 Write-Host "Not Present" -ForegroundColor Green  
337:            }  
338:       }  
339:       If ($Failed -eq $true) {  
340:            Return $false  
341:       } else {  
342:            Return $true  
343:       }  
344:  }  
345:    
346:  function Remove-RegistryKey {  
347:  <#  
348:       .SYNOPSIS  
349:            Delete registry key  
350:         
351:       .DESCRIPTION  
352:            Delete a registry key. If recurse is selected, all subkeys and values are deleted  
353:         
354:       .PARAMETER RegistryKey  
355:            Registry key to delete  
356:         
357:       .PARAMETER Recurse  
358:            Include all subkeys when deleting the registry key  
359:         
360:       .EXAMPLE  
361:            PS C:\> Remove-RegistryKey -RegistryKey 'Value1'  
362:         
363:       .NOTES  
364:            Additional information about the function.  
365:  #>  
366:         
367:       [CmdletBinding()]  
368:       param  
369:       (  
370:            [ValidateNotNullOrEmpty()][string]  
371:            $RegistryKey,  
372:            [switch]  
373:            $Recurse  
374:       )  
375:         
376:       $RegKey = "Registry::" + $RegistryKey  
377:       If ((Test-Path $RegKey) -eq $false) {  
378:            Write-Host $RegKey" already deleted"  
379:       } else {  
380:            $RegKeyItem = Get-Item $RegKey  
381:            If ($Recurse.IsPresent) {  
382:                 Write-Host "Recursive Deletion of"$RegKeyItem.PSChildName"....." -NoNewline  
383:                 Remove-Item $RegKey -Recurse -Force | Out-Null  
384:            } else {  
385:                 Write-Host "Deleting"$RegKeyItem.PSChildName"....." -NoNewline  
386:                 Remove-Item $RegKey -Force | Out-Null  
387:            }  
388:            If ((Test-Path $RegKey) -eq $false) {  
389:                 Write-Host "Success" -ForegroundColor Yellow  
390:            } else {  
391:                 $Failed = $true  
392:                 Write-Host "Failed" -ForegroundColor Red  
393:            }  
394:       }  
395:       If ($Failed -eq $true) {  
396:            Return $false  
397:       } else {  
398:            Return $true  
399:       }  
400:  }  
401:    
402:  function Set-ConsoleTitle {  
403:  <#  
404:       .SYNOPSIS  
405:            Console Title  
406:         
407:       .DESCRIPTION  
408:            Sets the title of the PowerShell Console  
409:         
410:       .PARAMETER ConsoleTitle  
411:            Title of the PowerShell Console  
412:         
413:       .NOTES  
414:            Additional information about the function.  
415:  #>  
416:         
417:       [CmdletBinding()]  
418:       param  
419:       (  
420:            [Parameter(Mandatory = $true)][String]  
421:            $ConsoleTitle  
422:       )  
423:         
424:       $host.ui.RawUI.WindowTitle = $ConsoleTitle  
425:  }  
426:    
427:  function Suspend-Service {  
428:  <#  
429:       .SYNOPSIS  
430:            Stop specified service  
431:         
432:       .DESCRIPTION  
433:            Stop a specified service and verify it is stopped  
434:         
435:       .PARAMETER Service  
436:            Name of the service  
437:         
438:       .EXAMPLE  
439:            PS C:\> Suspend-Service -Service 'Value1'  
440:         
441:       .NOTES  
442:            Additional information about the function.  
443:  #>  
444:         
445:       [CmdletBinding()][OutputType([boolean])]  
446:       param  
447:       (  
448:            [ValidateNotNullOrEmpty()][string]  
449:            $Service  
450:       )  
451:         
452:       $ServiceStatus = Get-Service $Service -WarningAction SilentlyContinue -ErrorAction SilentlyContinue  
453:       If ($ServiceStatus -ne $null) {  
454:            Write-Host "Stopping"$ServiceStatus.DisplayName"....." -NoNewline  
455:            If ($ServiceStatus.Status -ne 'Stopped') {  
456:                 Stop-Service -Name $Service -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -Force  
457:                 $ServiceStatus = Get-Service $Service -WarningAction SilentlyContinue -ErrorAction SilentlyContinue  
458:                 If ($ServiceStatus.Status -eq 'Stopped') {  
459:                      Write-Host "Success" -ForegroundColor Yellow  
460:                 } else {  
461:                      $Failed = $true  
462:                      Write-Host "Failed" -ForegroundColor Red  
463:                 }  
464:            } else {  
465:                 Write-Host "Service already stopped" -ForegroundColor Yellow  
466:            }  
467:       } else {  
468:            Write-Host $Service"service does not exist"  
469:       }  
470:       If ($Failed -eq $true) {  
471:            Return $false  
472:       } else {  
473:            Return $true  
474:       }  
475:  }  
476:    
477:  function Wait-ProcessEnd {  
478:  <#  
479:       .SYNOPSIS  
480:            Wait for a process to end  
481:         
482:       .DESCRIPTION  
483:            Pause the script until a process no longer exists  
484:         
485:       .PARAMETER ProcessName  
486:            Name of the process  
487:         
488:       .EXAMPLE  
489:                      PS C:\> Wait-ProcessEnd -ProcessName 'Value1'  
490:         
491:       .NOTES  
492:            Additional information about the function.  
493:  #>  
494:         
495:       [CmdletBinding()]  
496:       param  
497:       (  
498:            [ValidateNotNullOrEmpty()][string]  
499:            $ProcessName  
500:       )  
501:         
502:       $Process = Get-Process $ProcessName -ErrorAction SilentlyContinue  
503:       $Process = $Process | Where-Object { $_.ProcessName -eq $ProcessName }  
504:       Write-Host "Waiting for"$Process.Product"to complete....." -NoNewline  
505:       If ($Process -ne $null) {  
506:            Do {  
507:                 Start-Sleep -Seconds 2  
508:                 $Process = Get-Process $ProcessName -ErrorAction SilentlyContinue  
509:                 $Process = $Process | Where-Object { $_.ProcessName -eq $ProcessName }  
510:            }  
511:            While ($Process -ne $null)  
512:            Write-Host "Completed" -ForegroundColor Yellow  
513:       } else {  
514:            Write-Host "Process already completed" -ForegroundColor Yellow  
515:       }  
516:  }  
517:    
518:  cls  
519:  #Set the name of the powershell console  
520:  Set-ConsoleTitle -ConsoleTitle "SCCM Client"  
521:  #Skip over if the install directory and installer file are not defined  
522:  If ($ClientInstallationDirectory -ne $null) {  
523:       If ($ClientInstallationFile -ne $null) {  
524:            If ($ClientInstallationDirectory[$ClientInstallationDirectory.Length - 1] -ne '\') {  
525:                 $ClientInstallationDirectory += '\'  
526:            }  
527:            #Set the location and filename of the SCCM client installer  
528:            $File = $ClientInstallationDirectory + $ClientInstallationFile  
529:            #Get metadata from the SCCM client installer file  
530:            $FileMetaData = Get-MetaData -FileName $File  
531:       }  
532:  }  
533:  #Install parameter is defined  
534:  If ($Install.IsPresent) {  
535:       #Uninstall the SCCM client  
536:       $Parameters = "/uninstall"  
537:       $InstallStatus = Invoke-EXE -Uninstall -DisplayName $FileMetaData.Product -Executable $File -Switches $Parameters  
538:       If ($InstallStatus = $false) {  
539:            $Failed = $true  
540:       }  
541:       #Install the SCCM client  
542:       $Parameters = ""  
543:       If (($ManagementPoint -ne $null) -and ($ManagementPoint -ne "")) {  
544:            $Parameters += "/mp:" + $ManagementPoint  
545:       }  
546:       If (($SMSSiteCode -ne $null) -and ($SMSSiteCode -ne "")) {  
547:            If ($Parameters -ne "") {  
548:                 $Parameters += [char]32  
549:            }  
550:            $Parameters += "SMSSITECODE=" + $SMSSiteCode  
551:       }  
552:       If ($UsePKICert.IsPresent) {  
553:            If ($Parameters -ne "") {  
554:                 $Parameters += [char]32  
555:            }  
556:            $Parameters += "/UsePKICert"  
557:       }  
558:       If ($NOCRLCheck.IsPresent) {  
559:            If ($Parameters -ne "") {  
560:                 $Parameters += [char]32  
561:            }  
562:            $Parameters += "/NOCRLCheck"  
563:       }  
564:       If (($Source -ne $null) -and ($Source -ne "")) {  
565:            If ($Parameters -ne "") {  
566:                 $Parameters += [char]32  
567:            }  
568:            $Parameters += "/source:" + [char]34 + $Source + [char]34  
569:       }  
570:       $InstallStatus = Invoke-EXE -Install -DisplayName $FileMetaData.Product -Executable $File -Switches $Parameters  
571:       If ($InstallStatus -eq $false) {  
572:            $Failed = $true  
573:       }  
574:       #Uninstall parameter is defined  
575:  } elseif ($Uninstall.IsPresent) {  
576:       #Uninstall the SCCM client  
577:       $Parameters = "/Uninstall"  
578:       $InstallStatus = Invoke-EXE -Uninstall -DisplayName $FileMetaData.Product -Executable $File -Switches $Parameters  
579:       If ($InstallStatus -eq $false) {  
580:            $Failed = $true  
581:       }  
582:  }  
583:  #Build parameter is defined  
584:  If ($Build.IsPresent) {  
585:       #Stop the configuration manager client service  
586:       $InstallStatus = Suspend-Service -Service ccmexec  
587:       If ($InstallStatus -eq $false) {  
588:            $Failed = $true  
589:       }  
590:       #Delete the smscfg.ini file  
591:       $InstallStatus = Remove-File -Filename $env:windir"\smscfg.ini"  
592:       If ($InstallStatus -eq $false) {  
593:            $Failed = $true  
594:       }  
595:       #Delete the SCCM certificates from the registry  
596:       $InstallStatus = Remove-RegistryKey -RegistryKey "HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\SMS\Certificates" -Recurse  
597:       If ($InstallStatus -eq $false) {  
598:            $Failed = $true  
599:       }  
600:  }  
601:  If ($Failed -eq $true) {  
602:       $wshell = New-Object -ComObject Wscript.Shell  
603:       $wshell.Popup("Installation Failed", 0, "Installation Failed", 0x0)  
604:       Exit 1  
605:  } else {  
606:       Exit 0  
607:  }  
608:    

27 July 2016

PowerShell: Retrieving File Details

Posted By: Mick Pletcher - 10:42 AM













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:

video


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

Posted By: Mick Pletcher - 3:03 PM
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

Posted By: Mick Pletcher - 7:29 AM
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:    

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