Mick's IT Blogs

Mick's IT Blogs

Latest Updates

14 June 2017

MDT Windows Updates Build Report

Posted By: Mick Pletcher - 12:41 PM















I found it nice to be able to get a clean, filtered report on what Windows updates got installed during the build process. This allows me to inject those updates into the MDT Packages so they get injected into the image before it is laid down to speed the process up. I had published this tool two years ago and decided to revamp it to also include email functionality. The tool has given me a report, but there were times I forgot to look at it after a build completed. This reminds me by sending the report out via email.

The way this tool works is by reading the ZTIWindowsUpdate.log file from the c:\minint\smsosd\osdlogs directory and extracting the list of installed Windows Updates. The script filters out everything that is non-windows updates, such as Dell drivers. It also filters out the windows defender updates since those are cumulative and gets updated on a regular basis. 

This is a screenshot of what the logs look like when executed and output to the screen:



Here is a screenshot of what the same report looks like when opened up in Excel. 


The script extracts the KB article number and description and writes that information to an object. The object is then displayed on the screen and written to a .CSV file. It is sorted by KBArticle number. 

The firm I work at uses Dell machines and in doing so I excluded all Dell drivers from the list. There is also an exclusions.txt file it can read from to input items you may want to exclude from the list. I added "*Advanced Micro Devices*" as one item in my TXT file. The exclusions.txt file should reside in the same directory as the script. 

The script has been tested when a system is connected to the domain (Final Image) and when it belongs to a workgroup (Reference Image). It works in both instances.

I have pre-populated all parameters, except From, To, and SMTPServer. Those were left blank since you would likely want to populate them at the command line. 

Here is an example:

powershell.exe -file WindowsUpdatesReport.ps1 -email -From IT@Testcompany.com -To mickpletcher@testcompany.com -SMTPServer smtp.testcompany.com

I have pre-populated the -OutputFile, -ExclusionsFile, -Subject, and -Body. You can go into the script and change those or decide to override them by defining them at the command line. You could also populate the -From, -To, and -SMTPServer if you like. 

Here is a screenshot of how it is setup in the MDT task sequence the first time. This did not work. 


And this is a filtered screenshot of how it is setup under as an application install:



I tried one more way to execute it and it finally worked as shown below:


The command line I used is: powershell.exe -executionpolicy bypass -file <UNC path>\WindowsUpdatesReport.ps1 -Email -From <sender's email address> -To <recipient's email address> -SMTPServer <SMTP server address>

The start in contains the <UNC path> where the script resides.

You can download the file from my GitHub location


One more thing I wanted to mention is SAPIEN's PowerShell Studio. This made writing this script an absolute breeze. I highly recommend using it. 


 <#  
      .SYNOPSIS  
           Generate Windows Updates Report  
        
      .DESCRIPTION  
           This script will extract the list of windows updates installed  
           during an MDT build.  
        
      .PARAMETER OutputFile  
           File to write the list of installed updates to.  
        
      .PARAMETER ExclusionsFile  
           Text file containing a list of update descriptions to exclude from the report  
        
      .PARAMETER Email  
           Send an email to the specified IT staff with the attached .csv file containing a list of all updates installed during the build process.  
        
      .PARAMETER From  
           Email Sender  
        
      .PARAMETER To  
           Email Recipient  
        
      .PARAMETER SMTPServer  
           SMTPServer  
        
      .PARAMETER Subject  
           Email Subject  
        
      .PARAMETER Body  
           Body contents  
        
      .EXAMPLE  
           powershell.exe -executionpolicy bypass -file WindowsUpdatesReport.ps1 -OutputFile BaseBuild.csv -Path \\NetworkLocation\Directory  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:      5/31/2017 10:12 AM  
           Created by:      Mick Pletcher  
           Filename:        WindowsUpdatesReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$OutputFile = 'WindowsUpdatesReport.csv',  
      [ValidateNotNullOrEmpty()][string]$ExclusionsFile = 'Exclusions.txt',  
      [switch]$Email,  
      [string]$From,  
      [string]$To,  
      [string]$SMTPServer,  
      [string]$Subject = 'Windows Updates Build Report',  
      [string]$Body = "List of windows updates installed during the build process"  
 )  
   
 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 Remove-OutputFile {  
 <#  
      .SYNOPSIS  
           Delete Output File  
        
      .DESCRIPTION  
           This function deletes the old output file that contains a list of updates that were installed during a build.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Get the path this script is executing from  
      $RelativePath = Get-RelativePath  
      #Define location of the output file  
      $File = $RelativePath + $OutputFile  
      If ((Test-Path -Path $File) -eq $true) {  
           Remove-Item -Path $File -Force  
      }  
 }  
   
 function Get-Updates {  
 <#  
      .SYNOPSIS  
           Retrieve the list of installed updates  
        
      .DESCRIPTION  
           This function retrieves the list of updates that were installed during the build process  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([array])]  
      param ()  
        
      $UpdateArray = @()  
      #Get the path this script is executing from  
      $RelativePath = Get-RelativePath  
      #File containing a list of exclusions  
      $ExclusionsFile = $RelativePath + $ExclusionsFile  
      #Get list of exclusions from exclusions file  
      $Exclusions = Get-Content -Path $ExclusionsFile  
      #Locate the ZTIWindowsUpdate.log file  
      $FileName = Get-ChildItem -Path $env:HOMEDRIVE"\minint" -filter ztiwindowsupdate.log -recurse  
      #Get list of all installed updates except for Windows Malicious Software Removal Tool, Definition Update for Windows Defender, and Definition Update for Microsoft Endpoint Protection  
      $FileContent = Get-Content -Path $FileName.FullName | Where-Object { ($_ -like "*INSTALL*") } | Where-Object { $_ -notlike "*Windows Defender*" } | Where-Object { $_ -notlike "*Endpoint Protection*" } | Where-Object { $_ -notlike "*Windows Malicious Software Removal Tool*" } | Where-Object { $_ -notlike "*Dell*" } | Where-Object { $_ -notlike $Exclusions }  
      #Filter out all unnecessary lines  
      $Updates = (($FileContent -replace (" - ", "~")).split("~") | where-object { ($_ -notlike "*LOG*INSTALL*") -and ($_ -notlike "*ZTIWindowsUpdate*") -and ($_ -notlike "*-*-*-*-*") })  
      foreach ($Update in $Updates) {  
           #Create object  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           #Add KB article number to object  
           $Object | Add-Member -MemberType NoteProperty -Name KBArticle -Value ($Update.split("(")[1]).split(")")[0].Trim()  
           #Add description of KB article to object  
           $Description = $Update.split("(")[0]  
           $Description = $Description -replace (",", " ")  
           $Object | Add-Member -MemberType NoteProperty -Name Description -Value $Description  
           #Add the object to the array  
           $UpdateArray += $Object  
      }  
      If ($UpdateArray -ne $null) {  
           $UpdateArray = $UpdateArray | Sort-Object -Property KBArticle  
           #Define file to write the report to  
           $OutputFile = $RelativePath + $OutputFile  
           $UpdateArray | Export-Csv -Path $OutputFile -NoTypeInformation -NoClobber  
      }  
      Return $UpdateArray  
 }  
   
 Clear-Host  
 #Delete the old report file  
 Remove-OutputFile  
 #Get list of installed updates  
 Get-Updates  
 If ($Email.IsPresent) {  
      $RelativePath = Get-RelativePath  
      $Attachment = $RelativePath + $OutputFile  
      #Email Updates  
      Send-MailMessage -From $From -To $To -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Attachments $Attachment  
 }  
   

30 May 2017

Find Maximum Possible Resolution for Each Monitor

Posted By: Mick Pletcher - 3:47 PM















I have been working on a way that I can ensure the maximum resolution is set on monitors. Every so often, a monitor does not have the resolution set to maximum. I have been trying to figure out a way to set the resolution to maximum for more than a year, especially when a system is built in the build room and then moved to the user's office to be setup with new monitors. The problem I have had was being able to get the maximum resolution value. It is not stored in the system that is easily accessible through PowerShell. I had thought about having the script go through the keystroke process of the Display screen to set the monitor resolution to maximum and then write the resolution values to a text file. PowerShell can retrieve the resolution monitors are currently set to. Finally, it occurred to me the maximum resolution should be stored in the INF driver file. I opened up the file and there it was.

Andy Schneider has this awesome script that can set the resolution of the monitors. The only part was needed were the resolution values. The script below can be used with Andy's to set the resolution to maximum for each installed monitor. The Get-MaximumResolution function returns an array of objects containing the model, horizontal, and vertical resolutions.

Here is the output of the script after it was executed on my machine that has three monitors.



I also want to point out that SAPIEN's PowerShell Studio made writing the script a breeze. It simplifies the process of this and allows for much more thorough scripting.

The script is available to download from my GitHub site located here.


 <#  
      .SYNOPSIS  
           Get Maximum Monitor Resolution  
        
      .DESCRIPTION  
           This script will retrieve the maximum possible resolution for monitors by identifying the associated driver. The driver INF file contains the maximum defined resolution for a monitor. This script is designed for Dell monitors only. It has not been tested on any other brand. Also, the monitors need to be installed in the device manager to get the correct association.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/30/2017 12:37 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         MaxResolution.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Get-MaximumResolution {  
      #Create monitor array  
      $Monitors = @()  
      #Get associate monitor hardware ID for each monitor  
      $HardwareIDs = (Get-WmiObject Win32_PNPEntity | where-object { $_.PNPClass -eq "Monitor" }).HardwareID | ForEach-Object { $_.Split("\")[1] }  
      foreach ($Monitor in $HardwareIDs) {  
           #Create object  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           #Get the location of the associated driver file  
           $DriverFile = Get-ChildItem -path c:\windows\system32\driverstore -Filter *.inf -recurse | Where-Object { (Select-String -InputObject $_ -Pattern $Monitor -quiet) -eq $true }  
           #Retrieve the maximum resolution from the INF file  
           $MaxResolution = ((Get-Content -Path $DriverFile.FullName | Where-Object { $_ -like "*,,MaxResolution,,*" }).split('"')[1]).Split(",")  
           #Write the Model to the object  
           $Object | Add-Member -MemberType NoteProperty -Name Model -Value $DriverFile.BaseName.ToUpper()  
           #Write the horizontal maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Horizontal(X)" -Value $MaxResolution[0]  
           #Write the vertical maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Vertical(Y)" -Value $MaxResolution[1]  
           #Write the object to the array  
           $Monitors += $Object  
      }  
      Return $Monitors  
 }  
   
 #Display list of monitor with maximum available resolutions  
 $Monitors = Get-MaximumResolution  
 $Monitors  
   

23 May 2017

Pending Reboot Reporting with Orchestrator

Posted By: Mick Pletcher - 1:45 PM















As we are implementing the ADR in SCCM for servers, we want to know if systems are pending a reboot without having to log into every server. Thankfully, Kent Agerlund, formulated and posted this awesome solution for tracking pending reboots using a compliance rule and baseline in SCCM. It reports systems that are waiting for a reboot to a non-compliance collection.

I wanted to take this to the next level with automated reporting via email. I wrote the script below that queries the non-compliance collection in SCCM and writes the results to a .CSV file and emails that file to the appropriate IT staff. I integrated this with Orchestrator so this process becomes an automated process.

To implement this in Orchestrator, you will need to use the monitor date/time activity to schedule the execution. I have it scheduled for every day.



Next, you link a Run Program activity to run the PowerShell script.



The next activity is the Get File Status. This will check to see if the .CSV file exists. The PowerShell script will delete the old .CSV file and will not create a new one if there are no systems pending a reboot.



The next thing is to customize the link between the Get File Status and Send Email activities. This stops the Send Email from taking place if the .CSV file is not present.


Finally, the Send Email activity is executed to send an email to the appropriate IT staff with the attached .CSV file.


Here is a screenshot of my runbook.

I have included in the script an example of how to implement this in the command line. You can download the PowerShell script from my GitHub site located here.

I also wanted to say how much easier SAPIEN's PowerShell Studio made writing this script. PowerShell studio is a fantastic PowerShell editing tool that takes coding to a whole new level.

 <#  
      .SYNOPSIS  
           Reboot Report  
        
      .DESCRIPTION  
           This script will query SCCM for a list of machines pending a reboot. It will then write the list to a .CSV file.  
        
      .PARAMETER CollectionName  
           Name of the collection to query for a list of machines  
        
      .PARAMETER SCCMServer  
           Name of the SCCM Server  
        
      .PARAMETER SCCMDrive  
           Drive of the SCCM server  
        
      .PARAMETER ReportFile  
           Name of the file to write the list of systems pending a reboot.  
        
      .EXAMPLE  
           powershell.exe -file RebootReporting.ps1 -CollectionName "All Servers" -SCCMServer ACMESCCM -SCCMDrive CMG -ReportFile "PendingRebootReport.csv"  
   
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/22/2017 2:42 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         RebootReporting.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$CollectionName,  
      [ValidateNotNullOrEmpty()][string]$SCCMServer,  
      [ValidateNotNullOrEmpty()][string]$SCCMDrive,  
      [ValidateNotNullOrEmpty()][string]$ReportFile  
 )  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Get-RebootPendingSystems {  
 <#  
      .SYNOPSIS  
           Reboot Pending Systems  
        
      .DESCRIPTION  
           This function connects to SCCM and retrieves the list of systems pending a reboot.  
        
      .EXAMPLE  
                     PS C:\> Get-RebootPendingSystems  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Create Report array  
      $Report = @()  
      #If the SCCM drive does not have a colon at the end, add it  
      If ($SCCMDrive[$SCCMDrive.Length - 1] -ne ":") {  
           $SCCMDrive = $SCCMDrive + ":"  
      }  
      #Change the location to the SCCM drive  
      Set-Location $SCCMDrive  
      #Get list of systems in the SCCM collection that are pending a reboot  
      $Systems = (Get-CMDevice -collectionname $CollectionName).Name | Sort-object  
      foreach ($System in $Systems) {  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $System.ToUpper()  
           $Report += $Object  
      }  
      #Change location back to the system homedrive  
      Set-Location $env:HOMEDRIVE  
      #Return the list of systems  
      Return $Report  
 }  
   
 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  
 }  
   
 Clear-Host  
 #Find and import the PowerShell SCCM Module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Get a list of systems pending a reboot  
 $Report = Get-RebootPendingSystems  
 #Get the path this script is being executed from  
 $RelativePath = Get-RelativePath  
 #Add the relative path to the filename  
 $ReportFile = $RelativePath + $ReportFile  
 #Delete Report File if it exists  
 If ((Test-Path $ReportFile) -eq $true) {  
      Remove-Item -Path $ReportFile -Force  
 }  
 If (($Report -ne $null) -and ($Report -ne "")) {  
      #Display the list of systems to the screen  
      $Report  
      #Export the list of systems to a CSV file  
      $Report | Export-Csv -Path $ReportFile -Encoding UTF8 -Force -NoTypeInformation  
 }  
   

19 May 2017

Local Administrators Automated Reporting Tool

Posted By: Mick Pletcher - 3:04 PM















Back in November 2016, I posted the blog entry on reporting local administrators on machines. That script is deployed to machines via an SCCM package that reports the local administrators back to SCCM to be able to be queried into a report.

I got tired of having to go into SCCM and look at the query report to see if any new machines might have appeared. I decided to write this script that would automate the process. This script is designed to be used with System Center Orchestrator or it can be setup as a scheduled task. It is written to perform a query in SCCM to display the data the above-listed script reported to SCCM to the screen and to a CSV file.

I have set mine up in Orchestrator. Here is the process I used in setting mine up:

The first is setup to run this every day. The second points to the Daily Schedule properties defining which days to run it on. The third is the run program defining to run the PowerShell script. Here is how it is laid out:


The fourth one is defining to send an email with the CSV attachment. Here is how it is setup:


I wanted to take a moment and say how much SAPIEN PowerShell Studio made a difference with writing this code. It made the process so much easier and more efficient.

You can download the script from GitHub.


 <#  
      .SYNOPSIS  
           Execute SCCM Stored Queries  
        
      .DESCRIPTION  
           This script will execute SCCM stored queries.  
        
      .PARAMETER ListQueries  
           Generate a list of queries  
        
      .PARAMETER Query  
           Name of the query to execute  
        
      .PARAMETER SCCMServer  
           Name of SCCM server  
        
      .PARAMETER SCCMServerDrive  
           A description of the SCCMServerDrive parameter.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/8/2017 1:32 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMQuery.ps1  
           ===========================================================================  
 #>  
 param  
 (  
      [switch]$ListQueries,  
      [string]$Query,  
      [string]$SCCMServer,  
      [string]$SCCMServerDrive  
 )  
   
 function Get-ListOfQueries {  
 <#  
      .SYNOPSIS  
           Get List of Queries  
        
      .DESCRIPTION  
           This function will retrieve a list of all queries in SCCM  
        
      .EXAMPLE  
                     PS C:\> Get-ListOfQueries  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      Set-Location $SCCMServerDrive  
      $Queries = Get-CMQuery  
      Set-Location $env:SystemDrive  
      $QueryArray = @()  
      foreach ($Query in $Queries) {  
           $QueryArray += $Query.Name  
      }  
      $QueryArray = $QueryArray | Sort-Object  
      $QueryArray  
 }  
   
 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 Get-SCCMQueryData {  
      [CmdletBinding()]  
      param ()  
        
      $Report = @()  
      #Change directory to the SCCM server drive  
      Set-Location $SCCMServerDrive  
      #Retrieve report from SCCM  
      $Output = Get-CMQuery -Name $Query | Invoke-CMQuery  
      #Change directory back to the system this script is running on  
      Set-Location $env:SystemDrive  
      #Parse through data and create report object  
      foreach ($Item in $Output) {  
           $Item1 = [string]$Item  
           $Domain = (($Item1.split(';'))[0]).Split('"')[1]  
           $User = ((($Item1.split(";"))[1]).Split('"'))[1]  
           $ComputerName = ((($Item1.split(";"))[3]).Split('"'))[1]  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $ComputerName.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name Domain -Value $Domain.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name UserName -Value $User.ToUpper()  
           $Report += $Object  
      }  
      $Report = $Report | Sort-Object -Property UserName  
      Return $Report  
 }  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Send-Report {  
 <#  
      .SYNOPSIS  
           Email report  
        
      .DESCRIPTION  
           A detailed description of the Send-Report function.  
        
      .EXAMPLE  
                     PS C:\> Send-Report  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #TODO: Place script here  
 }  
   
 Clear-Host  
 #Add colon to end of SCCMServerDrive if not present  
 If ($SCCMServerDrive[$SCCMServerDrive.Length - 1] -ne ":") {  
      $SCCMServerDrive += ":"  
 }  
 #Import SCCM module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Generate a list of all available queries in SCCM  
 If ($ListQueries.IsPresent) {  
      Get-ListOfQueries  
 }  
 #If query is not filled in, then end the script  
 If (($Query -ne $null) -and ($Query -ne "")) {  
      #Perform query from SCCM  
      $Report = Get-SCCMQueryData | Sort-Object -Property ComputerName  
      #Display report to screen  
      $Report  
      #Get path where this script is executing from  
      $RelativePath = Get-RelativePath  
      #Location where to write the report to  
      $File = $RelativePath + "LocalAdministrators.csv"  
      #Delete old report if it exists  
      If ((Test-Path $File) -eq $true) {  
           Remove-Item -Path $File -Force  
      }  
      #Write new report to CSV file  
      $Report | Export-Csv -Path $File -Encoding UTF8 -Force  
 }  
   

27 April 2017

Automated Import of PowerShell SCCM Module

Posted By: Mick Pletcher - 3:25 PM















While writing quite a few PowerShell scripts for SCCM, I got tired of having to look up the location of the SCCM PowerShell module to import. I decided while writing the current PowerShell SCCM script, I would automate this process from now on. This makes it a snap using SAPIEN's PowerShell Studio to make this function a snippet and quickly add it each time I write a new SCCM script.

This script allows you to import the SCCM module without having to look up the location. The only parameter you have to specify is the -SCCMServer, which is the name of the SCCM server. The script will then connect to the SCCM server, get the parent installation directory, including the drive letter, and then search for the ConfigurationManager.psd1 file. If it finds more than one file, it will import the latest one.

You can use the function in this script in other scripts to make the process a breeze.

The script can be downloaded from my GitHub repository.


 <#  
      .SYNOPSIS  
           Imports the SCCM PowerShell Module  
        
      .DESCRIPTION  
           This function will import the SCCM PowerShell module without the need of knowing the location. The only thing that needs to be specified is the name of the SCCM server.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       4/26/2017 3:56 PM  
           Created by:       Mick Pletcher  
           Filename:         ImportSCCMModule.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 Import-SCCMModule -SCCMServer "SCCMServer"  
   

14 April 2017

Application Build Reporting for MDT and SCCM

Posted By: Mick Pletcher - 11:31 AM















A few years ago, I had published a PowerShell module that contained a function to log all app installs during a build process. With the project of upgrading systems to Windows 10, I decided to explore that once again.

This new script I have written is designed to query the programs and features registry entries to verify if an application is installed and to write that verification to a .CSV file. The purpose of this is to generate a report that is easy to open and see if all applications were successfully installed.

When I first started designing this script, I intended to have it read the ts.xml file from MDT and query the task sequence via PowerShell in SCCM. It was going to grab the name of the application that was exactly how it was listed in programs and features. That method had issues for the fact that some applications install more than one app during the installation. If you wanted to verify that more than one app was installed, then the script would not work correctly.

After much thought, I decided that writing a script in which I could designate in the parameters what application to search for would be much better. The script now becomes a task sequence after the installation sequence of the app. This allows a separate task for each app that an installation might install to be done.

Here is how I used the script in my task sequence. I build my reference images in MDT and deploy through SCCM. As you can see below, I have a task sequence to install that app and the next sequence is to check if it is installed and write to the CSV file.

Each of the task sequence checks are setup as Run PowerShell Scripts. In MDT, you will need to copy the PowerShell script to the %MDTDeploymentShare%\Scripts folder.
In the parameters field, you will need the following info:

  • -Application
  • -LogFileName
  • -LogFileLocation
  • -ExactFileName
The Application parameter will be the one that you will definitely need to define in the MDT task sequence because it will be different for each app. This parameter can be named either a partial name or the exact name as displayed in the Programs and Features. Such applications as Java Runtime can use a partial name 'Java 8' to get any version of the Java 8 that may be installed. Java 8 is always displayed Java 8u60 for instance.

The LogFileName parameter is what the logfile will be called. I define it in the task sequence because %OSDCOMPUTERNAME% is the computer name defined in the user input fields of an MDT image. If you hardcoded $env:computername to the parameter in the script, the computername you input will not be the same. 

The LogFileLocation parameter defines where you want the CSV file to be written.

The ExactFileName parameter specifies to search the programs and features for the name that matches exactly what is defined in the Application parameter. I ran into an instance with Microsoft Office 2016 where it had more than one entry. It still had two entries even using the exact name. In this case, the script measures the length of the registry entry and chooses the larger. I found if there are two entries with the exact names, the larger one is what is displayed in Programs and Features. 

Here is an example of Dell Command | Configure:


-Application 'Dell Command | Configure' -LogFileName %OSDCOMPUTERNAME% -LogFileLocation '\\Directory\ApplicationBuildReport\Reports'

This is what the CSV file report looks like after a reference build is completed. Some of the registry entries include the version within the filename.

As you can see, it makes for an easy way to check if everything got installed correctly in a build. One more thing I do in my reference image task sequence is to suspend the imaging process so I can check this report before a reference image is generated. That way, if items are missing, I can either install them manually and then proceed with grabbing an image or fix the problem and then restart the image all over.

You can download the script from my GitHub repository located here.


 <#  
      .SYNOPSIS  
           Check Application Install  
        
      .DESCRIPTION  
           This script will check if a specified application appears in the programs and features.  
        
      .PARAMETER Application  
           Name of application in the programs and features. It can be a partial name or the complete name.  
        
      .PARAMETER LogFileName  
           Name of the LogFileName containing the list of applications installed on the machine  
        
      .PARAMETER LogFileLocation  
           Location where to write the log file  
        
      .PARAMETER ExactFileName  
           Specifies to search for the exact filename, otherwise the script will search for filename that contain the designated search criteria.  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.138  
           Created on:      4/4/2017 4:51 PM  
           Created by:      Mick Pletcher  
           Filename:        AppChecker.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$Application,  
      [ValidateNotNullOrEmpty()][string]$LogFileName,  
      [string]$LogFileLocation,  
      [switch]$ExactFileName  
 )  
   
 function New-LogFile {  
 <#  
      .SYNOPSIS  
           Create new build log  
        
      .DESCRIPTION  
           This function will compare the date/time of the first event viewer log with the date/time of the log file to determine if it needs to be deleted and a new one created.  
        
      .PARAMETER LogFile  
           Full name of log file including the unc address  
        
      .EXAMPLE  
           PS C:\> New-LogFile  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$Log  
      )  
        
      #Get the creation date/time of the first event viewer log  
      $LogFile = Get-ChildItem -Path $LogFileLocation -Filter $LogFileName -ErrorAction SilentlyContinue  
      Write-Output "Log File Name: $LogFile"  
      $Output = "LogFile Creation Date: " + $LogFile.CreationTime  
      Write-Output $Output  
      If ($LogFile -ne $null) {  
           $OSInstallDate = Get-WmiObject Win32_OperatingSystem | ForEach-Object{ $_.ConvertToDateTime($_.InstallDate) -f "MM/dd/yyyy" }  
           Write-Output "    OS Build Date: $OSInstallDate"  
           If ($LogFile.CreationTime -lt $OSInstallDate) {  
                #Delete old log file  
                Remove-Item -Path $LogFile.FullName -Force | Out-Null  
                #Create new log file  
                New-Item -Path $Log -ItemType File -Force | Out-Null  
                #Add header row  
                Add-Content -Path $Log -Value "Application,Version,TimeStamp,Installation"  
           }  
      } else {  
           #Create new log file  
           New-Item -Path $Log -ItemType File -Force | Out-Null  
           #Add header row  
           Add-Content -Path $Log -Value "Application,Version,TimeStamp,Installation"  
      }  
 }  
   
 Clear-Host  
 #If the LogFileName is not predefined in the Parameter, then it is named after the computer name  
 If (($LogFileName -eq $null) -or ($LogFileName -eq "")) {  
      If ($LogFileName -notlike "*.csv*") {  
           $LogFileName += ".csv"  
      } else {  
           $LogFileName = "$env:COMPUTERNAME.csv"  
      }  
 } elseIf ($LogFileName -notlike "*.csv*") {  
           $LogFileName += ".csv"  
 }  
 #Add backslash to end of UNC path  
 If ($LogFileLocation[$LogFileLocation.Length - 1] -ne "\") {  
      $File = $LogFileLocation + "\" + $LogFileName  
 } else {  
      $File = $LogFileLocation + $LogFileName  
 }  
 #Create a new log file  
 New-LogFile -Log $File  
 #Get list of installed applications from programs and features  
 $Uninstall = Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue  
 $Uninstall += Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue  
 #Check if the list of Applications contains the search query  
 If ($ExactFileName.IsPresent) {  
      $ApplicationInstall = $Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -eq $Application }  
 } else {  
      $ApplicationInstall = $Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -like "*" + $Application + "*" }  
 }  
 #If more than one registry entry, select larger entry that contains more information  
 If ($ApplicationInstall.length -gt 1) {  
      $Size = 0  
      for ($i = 0; $i -lt $ApplicationInstall.length; $i++) {  
           If (([string]$ApplicationInstall[$i]).length -gt $Size) {  
                $Size = ([string]$ApplicationInstall[$i]).length  
                $Temp = $ApplicationInstall[$i]  
           }  
      }  
      $ApplicationInstall = $Temp  
 }  
 #Exit with error code 0 if the app is installed, otherwise exit with error code 1  
 If ($ApplicationInstall -ne $null) {  
      $InstallDate = (($ApplicationInstall.InstallDate + "/" + $ApplicationInstall.InstallDate.substring(0, 4)).Substring(4)).Insert(2, "/")  
      $Output = $ApplicationInstall.DisplayName + "," + $ApplicationInstall.Version + "," + $InstallDate + "," + "Success"  
      Add-Content -Path $File -Value $Output  
      Write-Host "Exit Code: 0"  
      Exit 0  
 } else {  
      $Output = $Application + "," + "," + "," + "Failed"  
      Add-Content -Path $File -Value $Output  
      Write-Host "Exit Code: 1"  
      Exit 1  
 }  
   

29 March 2017

SCCM Active Directory Old and Corrupt System Reporting Tool

Posted By: Mick Pletcher - 11:04 AM















We wanted a comprehensive report of systems to be automatically generated on a monthly basis with the following information:


  • System Name
  • IP Address
  • Last Logon Time Stamp
  • Is it pingable?
  • Is the SCCM client installed
  • Is the SCCM client active
  • Last active time stamp of the SCCM client
I decided to write this script that combines DNS, SCCM, and AD information into one comprehensive report that can be filtered in Excel. 

The script requires three PowerShell modules to work:
  • ConfigurationManager.psd1
  • DNSClient.psd1
  • ActiveDirectory.psd1
On the system you will be executing this script from, RSAT will be required to execute it. 

The script will take a while to execute, depending on how many systems are in your environment. Here is a sample screenshot I took after running the report:


As you can see in the screenshot, the data can be very helpful in finding systems that may be gone, but were not deleted in AD and/or SCCM. It can also show you systems were the client may be busted. The report is generated and placed in the same location as the script. 

While writing this script, I used SAPIEN's PowerShell Studio and must say that it made writing this a breeze. It helps in documenting and making the code much more efficient.

You can download the script from my GitHub site

 <#  
      .SYNOPSIS  
           System Status Reporting Tool  
        
      .DESCRIPTION  
           This script will generate a report that pulls from the SCCM All Systems collection, active directory, and the DNS. The script will first get a list of All Systems that is typically populated by active directory. It will then iterate through the list getting the system name from SCCM, IP address from DNS, last logon time stamp from AD, if it is pingable from PowerShell, if the SCCM client is installed, if the client is active, and the last active time of the client. This information is put in a report form and written both to a CSV file and to the display. The report will show systems without the SCCM client, systems that have not been online for a long time, and systems that may have a corrupt client.  
        
      .PARAMETER SiteCode  
           SCCM site code needed to execute the configuration manager cmdlets.  
        
      .PARAMETER SCCMModule  
           Path to the configuration manager PowerShell module  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.136  
           Created on:       3/28/2017 9:54 AM  
           Created by:       Mick Pletcher  
           Filename:         SCCMADReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$SiteCode,  
      [ValidateNotNullOrEmpty()][string]$SCCMModule  
 )  
 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  
 }  
   
 Clear-Host  
 Import-Module DnsClient  
 Import-Module ActiveDirectory  
 Import-Module -Name $SCCMModule  
 #Format sitecode  
 If ($SiteCode[$SiteCode.Length - 1] -ne ":") {  
      $SiteCode = $SiteCode + ":"  
 }  
 #Get location of current powershell connection  
 $Location = (get-location).Path.Split("\")[0]  
 #Change connection to configuration manager  
 Set-Location $SiteCode  
 #Get list of all systems in SCCM  
 $Systems = Get-CMDevice -CollectionName "All Systems" | Where-Object { $_.Name -notlike "*Unknown Computer*" }  
 #Create Reports array  
 $Report = @()  
 foreach ($System in $Systems) {  
      #Get SCCM info for $System  
      $SCCMSystemInfo = $Systems | Where-Object { $_.Name -eq $System.Name }  
      #Get the last logon timestamp from active directory  
      Try {  
           $LLTS = [datetime]::FromFileTime((get-adcomputer $System.Name -properties LastLogonTimeStamp -ErrorAction Stop).LastLogonTimeStamp).ToString('d MMMM yyyy')  
      } Catch {  
           $Output = $System.Name + " is not in active directory"  
           Write-Output $Output  
      }  
      #Test if the system is pingable  
      $Pingable = Test-Connection -ComputerName $System.Name -Count 2 -Quiet  
      #Get the ipaddress for the system  
      Try {  
           $IPAddress = (Resolve-DnsName -Name $System.Name -ErrorAction Stop).IPAddress  
      } Catch {  
           $Output = $System.Name + " IP address cannot be resolved"  
           Write-Output $Output  
      }  
      $Object = New-Object -TypeName System.Management.Automation.PSObject  
      $Object | Add-Member -MemberType NoteProperty -Name Name -Value $System.Name  
      $Object | Add-Member -MemberType NoteProperty -Name IPAddress -Value $IPAddress  
      $Object | Add-Member -MemberType NoteProperty -Name ADLastLogon -Value $LLTS  
      $Object | Add-Member -MemberType NoteProperty -Name Pingable -Value $Pingable  
      $Object | Add-Member -MemberType NoteProperty -Name SCCMClient -Value $SCCMSystemInfo.IsClient  
      $Object | Add-Member -MemberType NoteProperty -Name SCCMActive -Value $SCCMSystemInfo.IsActive  
      $Object | Add-Member -MemberType NoteProperty -Name SCCMLastActiveTime -Value $SCCMSystemInfo.LastActiveTime  
      $Report += $Object  
      #Clear variables if they exist so previous data is not used for systems that have null values  
      If ($IPAddress) {  
           Remove-Variable -Name IPAddress -Force  
      }  
      If ($LLTS) {  
           Remove-Variable -Name LLTS -Force  
      }  
      If ($Pingable) {  
           Remove-Variable -Name Pingable -Force  
      }  
      If ($SCCMInfo) {  
           Remove-Variable -Name SCCMInfo -Force  
      }  
 }  
 #Change connection to local system  
 Set-Location $Location  
 Clear-Host  
 #Sort report by computer name  
 $Report = $Report | Sort-Object -Property Name  
 #Get the path this script is being executed from  
 $RelativePath = Get-RelativePath  
 #Path and filename to write the report to  
 $File = $RelativePath + "SCCMReport.csv"  
 #Delete old report file  
 If ((Test-Path $File) -eq $true) {  
      Remove-Item -Path $File -Force  
 }  
 #Write report to CSV file  
 $Report | Export-Csv -Path $File -Encoding UTF8 -Force  
 #Write Report to screen  
 $Report | Format-Table  

22 March 2017

PowerShell: Disable Cisco Jabber History

Posted By: Mick Pletcher - 3:49 PM















I published a blog posting on how to securely delete the Cisco Jabber conversation using the PowerShell script I had written. It occurred to me that the .DB file which contains the conversations could be prevented from recording in the first place. This is done by setting the .DB file to read-only. In order to do this, there is a specific process that needs to take place, especially if this is being done to systems where conversations have already taken place, otherwise you would have old conversations permanently left in the .DB file.

The .DB file needs to be deleted and then recreated before setting it to read-only. This process can only occur after a user is logged in because of the .DB file is stored within the user profile. Jabber is first closed out. Once closed, the .DB file is deleted. Jabber is then opened back up to recreate the .DB file. Jabber is once again closed. The .DB file is not set to read-only and Jabber is reopened once again.

After I fomulated this process, I used SAPIEN's PowerShell Studio to write the code. PowerShell Studio made writing this a breeze and as you can see in the code, it is very clean and well documented due to the app.

Here is a video clip of the script running on my own machine.


The script can be implemented through a package in SCCM or it can be setup to execute from a share as a run once registry entry when the user logs in the first time. SCCM is probably the best option as it does not execute the script immediately. It jabber has not been launched the first time before the script runs, the script will fail because of the .DB file will not be present. Another issue I encountered was executing the script. It must be executed in the 32-bit PowerShell. If executed in 64-Bit PowerShell, the add/remove programs lookup will fail.

You can download the script from my GitHub site located here.


 <#  
      .SYNOPSIS  
           Disable Cisco Jabber Chat History  
        
      .DESCRIPTION  
           This script will disable the Jabber chat history by setting the .DB file to read-only. It begins by killing the Jabber task, deleting the .DB file, reopening Jabber, and then setting the .DB file to read-only. The script uses this process because if the old .DB file has not been deleted before setting it to read-only, the stored conversations will be there permanently. Since the .DB file is stored in the user profile, this cannot be done in the build.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.3.131  
           Created on:       03/21/2017 4:21 PM  
           Created by:       Mick Pletcher  
           Filename:         CiscoJabberChatCleanup.ps1  
           ===========================================================================  
 #>  
   
 [CmdletBinding()]  
 param ()  
   
 function Close-Process {  
 <#  
      .SYNOPSIS  
           Stop ProcessName  
        
      .DESCRIPTION  
           Kills a ProcessName and verifies it was stopped while reporting it back to the screen.  
        
      .PARAMETER ProcessName  
           Name of ProcessName to kill  
        
      .EXAMPLE  
           PS C:\> Close-ProcessName -ProcessName 'Value1'  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([boolean])]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$ProcessName  
      )  
        
      $Process = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue  
      If ($Process -ne $null) {  
           $Output = "Stopping " + $Process.Name + " process....."  
           Stop-Process -Name $Process.Name -Force -ErrorAction SilentlyContinue  
           Start-Sleep -Seconds 1  
           $TestProcess = Get-Process $ProcessName -ErrorAction SilentlyContinue  
           If ($TestProcess -eq $null) {  
                $Output += "Success"  
                Write-Host $Output  
                Return $true  
           } else {  
                $Output += "Failed"  
                Write-Host $Output  
                Return $false  
           }  
      } else {  
           Return $true  
      }  
 }  
   
 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 Open-Application {  
 <#  
      .SYNOPSIS  
           Open Application  
        
      .DESCRIPTION  
           Opens an applications  
        
      .PARAMETER Executable  
           A description of the Executable parameter.  
        
      .PARAMETER ApplicationName  
           Display Name of the application  
        
      .PARAMETER Process  
           Application Process Name  
        
      .EXAMPLE  
           PS C:\> Open-Application -Executable 'Value1'  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [string]$Executable,  
           [ValidateNotNullOrEmpty()][string]$ApplicationName  
      )  
        
      $Architecture = Get-Architecture  
      $Uninstall = Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"  
      }  
      $InstallLocation = ($Uninstall | ForEach-Object { Get-ItemProperty $_.PsPath } | Where-Object { $_.DisplayName -eq $ApplicationName }).InstallLocation  
      If ($InstallLocation[$InstallLocation.Length - 1] -ne "\") {  
           $InstallLocation += "\"  
      }  
      $Process = ($Executable.Split("."))[0]  
      $Output = "Opening $ApplicationName....."  
      Start-Process -FilePath $InstallLocation$Executable -ErrorAction SilentlyContinue  
      Start-Sleep -Seconds 5  
      $NewProcess = Get-Process $Process -ErrorAction SilentlyContinue  
      If ($NewProcess -ne $null) {  
           $Output += "Success"  
      } else {  
           $Output += "Failed"  
      }  
      Write-Output $Output  
 }  
   
 function Remove-ChatFiles {  
 <#  
      .SYNOPSIS  
           Delete Jabber Chat Files  
        
      .DESCRIPTION  
           Deletes Jabber chat files located at %USERNAME%\AppData\Local\Cisco\Unified Communications\Jabber\CSF\History and verifies they were deleted  
        
      .EXAMPLE  
           PS C:\> Remove-ChatFiles  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Get Jabber Chat history files  
      $ChatHistoryFiles = Get-ChildItem -Path $env:USERPROFILE'\AppData\Local\Cisco\Unified Communications\Jabber\CSF\History' -Filter *.db  
      If ($ChatHistoryFiles -ne $null) {  
           foreach ($File in $ChatHistoryFiles) {  
                $Output = "Deleting " + $File.Name + "....."  
                Remove-Item -Path $File.FullName -Force | Out-Null  
                If ((Test-Path $File.FullName) -eq $false) {  
                     $Output += "Success"  
                } else {  
                     $Output += "Failed"  
                }  
           }  
           Write-Output $Output  
      } else {  
           $Output = "No Chat History Present"  
           Write-Output $Output  
      }  
 }  
   
 function Remove-MyJabberFilesFolder {  
 <#  
      .SYNOPSIS  
           Delete MyJabberFiles Folder  
        
      .DESCRIPTION  
           Delete the MyJabberFiles folder stores under %USERNAME%\documents and verifies it was deleted.  
        
      .EXAMPLE  
           PS C:\> Remove-MyJabberFilesFolder  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      $MyJabberFilesFolder = Get-Item $env:USERPROFILE'\Documents\MyJabberFiles' -ErrorAction SilentlyContinue  
      If ($MyJabberFilesFolder -ne $null) {  
           $Output = "Deleting " + $MyJabberFilesFolder.Name + "....."  
           Remove-Item -Path $MyJabberFilesFolder -Recurse -Force | Out-Null  
           If ((Test-Path $MyJabberFilesFolder.FullName) -eq $false) {  
                $Output += "Success"  
           } else {  
                $Output += "Failed"  
           }  
           Write-Output $Output  
      } else {  
           $Output = "No MyJabberFiles folder present"  
           Write-Output $Output  
      }  
 }  
   
 function Set-DBFilePermissions {  
 <#  
      .SYNOPSIS  
           Set .DB File Permission  
        
      .DESCRIPTION  
           Make the .DB file read-only  
        
      .EXAMPLE  
                     PS C:\> Set-DBFilePermissions  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Get list of chat history files  
      $ChatHistoryFiles = Get-ChildItem -Path $env:USERPROFILE'\AppData\Local\Cisco\Unified Communications\Jabber\CSF\History' -Filter *.db  
      foreach ($File in $ChatHistoryFiles) {  
           #Set .DB file to read-only  
           $Output = "Setting " + $File.Name + " to Read-Only....."  
           $ReadOnly = Get-ItemPropertyValue -Path $File.FullName -Name IsReadOnly  
           If (($ReadOnly) -eq $false) {  
                Set-ItemProperty -Path $File.FullName -Name IsReadOnly -Value $true  
                $ReadOnly = Get-ItemPropertyValue -Path $File.FullName -Name IsReadOnly  
                If (($ReadOnly) -eq $true) {  
                     $Output += "Success"  
                } else {  
                     $Output += "Failed"  
                }  
           } else {  
                $Output += "Success"  
           }  
           Write-Output $Output  
      }  
 }  
   
 Clear-Host  
 #Kill Cisco Jabber Process  
 $JabberClosed = Close-Process -ProcessName CiscoJabber  
 #Delete .DB files from %USERNAME%\AppData\Local\Cisco\Unified Communications\Jabber\CSF\History  
 Remove-ChatFiles  
 #Delete %USERNAME%\documents\MyJabberFiles directory  
 Remove-MyJabberFilesFolder  
 #Reopen Jabber if it was open  
 If ($JabberClosed -eq $true) {  
      Open-Application -ApplicationName "Cisco Jabber" -Executable CiscoJabber.exe  
 }  
 $JabberClosed = Close-Process -ProcessName CiscoJabber  
 #Set the .DB file to read-only  
 Set-DBFilePermissions  
 #Reopen Jabber  
 If ($JabberClosed -eq $true) {  
      Open-Application -ApplicationName "Cisco Jabber" -Executable CiscoJabber.exe  
 }  
   

16 March 2017

PowerShell: Generate User Logon Report

Posted By: Mick Pletcher - 4:26 PM















This script will generate a logon report of a specific user on a specific machine. This script is designed to query the event viewer logs on either a local or remote machine. It does not require WinRM for this to occur.

The script begins by querying the registry or remote registry to find the associated SID with the specified user profile. It then proceeds to retrieve all Event 4624 IDs from the event viewer logs. It filters those logs into four categories: Keyboard Logins, Unlock, Remote, and Cached Credentials. This website helped me considerably in knowing how to generate and classify the reporting.

The script will write the output to a CSV file, categorizing the logon times by the four categories listed above. If the -Rawdata parameter is used, the script will also write the detailed message data with a timestamp on each data entry to a TXT file.

Command line execution to get a formatted CSV file:

  • powershell.exe -file LogonTimes.ps1 -ComputerName "Test01" -Username "User01"
Command line execution to get a formatted CSV and raw data TXT file:
  • powershell.exe -file LogonTimes.ps1 -ComputerName "Test01" -Username "User01" -Rawdata
Command line execution to get a formatted CSV file on the local system:
  • powershell.exe -file LogonTimes.ps1 -Username "User01"
You can download the script from GitHub

PowerShell Studio made writing this script a breeze. If you look at the script and see all of the documentation in it, that is because PowerShell Studio makes that very easy and quick. It is well worth the money to purchase this tool. 

This is a view of a success execution of the script:


LogonTimes.ps1

 <#  
      .SYNOPSIS  
           User Logon Report  
        
      .DESCRIPTION  
           This script will query the event viewer logs of a specified system for a list of logon times for a specific user. There are four fields in the report: Keyboard logons, Screen Unlock, Remote Session logons, and Cached Logon. It has the option to either generate a report in a CSV file with all of the above field data, or it can generate a TXT file containing the raw message data with each data field split off by two dash rows.  
             
           NOTE: This does not require WinRM to be enabled to run on external systems. Also, this can take quite a while to execute if the logs are really big.  
        
      .PARAMETER ComputerName  
           Name of system to retrieve the logs from. If this is left blank, the script will use "." representing the computer this script is executing on.  
        
      .PARAMETER Rawdata  
           Generate a report using the raw data from the event viewer logs of the specified user  
        
      .PARAMETER Username  
           Username to generate this report of.  
        
      .EXAMPLE  
           Generate a CSV file report containing the times and sorted by each logon type  
           powershell.exe -file LogonTimes.ps1 -Username MickPletcher -ComputerName PC01  
             
           Generate a TXT file that contains all of the raw message data fields for the specified system  
           powershell.exe -file LogonTimes.ps1 -Username MickPletcher -ComputerName PC01 -Rawdata  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.136  
           Created on:       3/15/2017 12:00 PM  
           Created by:       Mick Pletcher  
           Filename:         LogonTimes.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [String]$ComputerName,  
      [switch]$Rawdata,  
      [ValidateNotNullOrEmpty()][string]$Username  
 )  
   
 function Get-FilteredData {  
 <#  
      .SYNOPSIS  
           Filter By LogonType Type  
        
      .DESCRIPTION  
           This will filter the data for the specified LogonType type  
        
      .PARAMETER LogonType  
           Specified LogonType type  
        
      .PARAMETER Message  
           Message to display on the screen  
        
      .PARAMETER Logons  
           Array containing all logons  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$LogonType,  
           [ValidateNotNullOrEmpty()][string]$Message,  
           [ValidateNotNullOrEmpty()]$Logons  
      )  
        
      $Errors = $false  
      Write-Host $Message"....." -NoNewline  
      Try {  
           $Data = $Logons | Where-Object { $_.Message -like "*Logon Type*"+[char]9+[char]9+$LogonType+"*" }  
      } catch {  
           $Errors = $true  
      }  
      If ($Errors -eq $false) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
      Return $Data  
 }  
   
 function Get-SID {  
 <#  
      .SYNOPSIS  
           Retrieve SID from HKEY_LOCAL_MACHINE  
        
      .DESCRIPTION  
           This script will retrieve the SID by querying the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList by matching the ProfileImagePath value with the Username parameter.   
        
      .EXAMPLE  
           PS C:\> Get-SID  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      Write-Host "Retrieving SID for $Username....." -NoNewline  
      If ($ComputerName -eq ".") {  
           #Get associated SID of User Profile  
           $SID = (get-childitem -path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | Where-Object { $_.Name -like "*S-1-5-21*" } | ForEach-Object { Get-ItemProperty REGISTRY::$_ } | Where-Object { $_.ProfileImagePath -like "*$Username*" }).PSChildName  
      } else {  
           $HKEY_LOCAL_MACHINE = 2147483650  
           $Key = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"  
           $RegClass = Get-WMIObject -Namespace "Root\Default" -List -ComputerName $ComputerName | Where-object { $_.Name -eq "StdRegProv" }  
           $Value = "ProfileImagePath"  
           $SID = ($RegClass.EnumKey($HKEY_LOCAL_MACHINE, $Key)).sNames | Where-Object { $_ -like "*S-1-5-21*" } | ForEach-Object {  
                If (($RegClass.GetStringValue($HKEY_LOCAL_MACHINE, $Key + "\" + $_, $Value)).sValue -like "*" + $Username + "*") {  
                     $_  
                }  
           }  
      }  
      If (($SID -ne "") -and ($SID -ne $null)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
      Return $SID  
 }  
   
 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 New-Report {  
 <#  
      .SYNOPSIS  
           Generate CSV Report File  
        
      .DESCRIPTION  
           This function will generate a CSV report.  
        
      .PARAMETER Keyboard  
           A description of the Keyboard parameter.  
        
      .PARAMETER Unlock  
           A description of the Unlock parameter.  
        
      .PARAMETER Remote  
           A description of the Remote parameter.  
        
      .PARAMETER Cached  
           A description of the Cached parameter.  
        
      .EXAMPLE  
                     PS C:\> New-Report  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           $Keyboard,  
           $Unlock,  
           $Remote,  
           $Cached  
      )  
        
      $RelativePath = Get-RelativePath  
      #Name of report file  
      $FileName = $RelativePath + "$Username.csv"  
      #Delete report file if it exists  
      If ((Test-Path $FileName) -eq $true) {  
           Write-Host "Deleting $Username.csv....." -NoNewline  
           Remove-Item -Path $FileName -Force  
           If ((Test-Path $FileName) -eq $false) {  
                Write-Host "Success" -ForegroundColor Yellow  
           } else {  
                Write-Host "Failed" -ForegroundColor Red  
           }  
      }  
      Write-Host "Generating $Username.csv report file....." -NoNewline  
      #Create new file  
      "Logon Type,Date/Time" | Out-File -FilePath $FileName -Encoding UTF8 -Force  
      $Errors = $false  
      #Report all keyboard logons  
      foreach ($Logon in $Keyboard) {  
           $Item = "Keyboard," + [string]$Logon.TimeCreated  
           try {  
                $Item | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           } catch {  
                $Errors = $true  
           }  
      }  
      #Report all screen unlocks  
      foreach ($Logon in $Unlock) {  
           $Item = "Unlock," + [string]$Logon.TimeCreated  
           Try {  
                $Item | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           } catch {  
                $Errors = $true  
           }  
      }  
      #Report all remote logons  
      foreach ($Logon in $Remote) {  
           $Item = "Remote," + [string]$Logon.TimeCreated  
           Try {  
                $Item | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           } catch {  
                $Errors = $true  
           }  
      }  
      #Report all cached logons  
      foreach ($Logon in $Cached) {  
           $Item = "Cached," + [string]$Logon.TimeCreated  
           Try {  
                $Item | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           } catch {  
                $Errors = $true  
           }  
      }  
      If ($Errors -eq $false) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
 }  
   
 function Get-LogonLogs {  
 <#  
      .SYNOPSIS  
           Retrieve all Logon Logs from Event Viewer  
        
      .DESCRIPTION  
           This function will query the event viewer for all Event ID 4624, filtered with the user's SID.  
        
      .PARAMETER SID  
           User's SID  
        
      .EXAMPLE  
           PS C:\> Get-LogonLogs  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()]$SID  
      )  
        
      If ($ComputerName -ne ".") {  
           Write-Host "Retrieving all logon logs for $Username on $ComputerName....." -NoNewline  
      } else {  
           Write-Host "Retrieving all logon logs for $Username on $env:COMPUTERNAME....." -NoNewline  
      }  
      $Errors = $false  
      Try {  
           If ($ComputerName -ne ".") {  
                $AllLogons = Get-WinEvent -FilterHashtable @{ logname = 'security'; ID = 4624 } -ComputerName $ComputerName | where-object { ($_.properties.value -like "*$SID*") }  
           } else {  
                $AllLogons = Get-WinEvent -FilterHashtable @{ logname = 'security'; ID = 4624 } | where-object { ($_.properties.value -like "*$SID*") }  
           }  
      } catch {  
           $Errors = $true  
      }  
      If ($Errors -eq $false) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
      Return $AllLogons  
 }  
   
 #******************************************************************************  
 #******************************************************************************  
   
 Clear-Host  
 If (($ComputerName -eq "") -or ($ComputerName -eq $null)) {  
      $ComputerName = "."  
 }  
 $SID = Get-SID  
 #Retrieve all logon logs  
 $AllLogons = Get-LogonLogs -SID $SID  
 #Logon at keyboard and screen of system  
 $KeyboardLogons = Get-FilteredData -Logons $AllLogons -LogonType "2" -Message "Filtering keyboard logons"  
 #Unlock workstation with password protected screen saver  
 $Unlock = Get-FilteredData -Logons $AllLogons -LogonType "7" -Message "Filtering system unlocks"  
 #Terminal Services, Remote Desktop or Remote Assistance  
 $Remote = Get-FilteredData -Logons $AllLogons -LogonType "10" -Message "Filtering remote accesses"  
 #logon with cached domain credentials such as when logging on to a laptop when away from the network  
 $CachedCredentials = Get-FilteredData -Logons $AllLogons -LogonType "11" -Message "Filtering cached logins"  
 #Generate a rawdata report  
 If ($Rawdata.IsPresent) {  
      $RelativePath = Get-RelativePath  
      #Name of report file  
      $FileName = $RelativePath + "$Username.txt"  
      #Delete report file if it exists  
      If ((Test-Path $FileName) -eq $true) {  
           Write-Host "Deleting $Username.txt....." -NoNewline  
           Remove-Item -Path $FileName -Force  
           If ((Test-Path $FileName) -eq $false) {  
                Write-Host "Success" -ForegroundColor Yellow  
           } else {  
                Write-Host "Failed" -ForegroundColor Red  
           }  
      }  
      Write-Host "Generating raw data file....." -NoNewline  
      foreach ($Logon in $AllLogons) {  
           [string]$Logon.TimeCreated | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           $Logon.Message | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           " " | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           "----------------------------------------------------------------------------------------------------------------------------------------------------------------" | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           "----------------------------------------------------------------------------------------------------------------------------------------------------------------" | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
           " " | Out-File -FilePath $FileName -Encoding UTF8 -Append -Force  
      }  
      If ((Test-Path $FileName) -eq $true) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
 } else {  
      New-Report -Keyboard $KeyboardLogons -Unlock $Unlock -Remote $Remote -Cached $CachedCredentials  
 }  

06 March 2017

PowerShell: Uninstall MSI by Application Name

Posted By: Mick Pletcher - 3:14 PM















Here is a function that will uninstall an MSI installed application by the name of the app. You do not need to input the entire name either. For instance, say you are uninstalling all previous versions of Adobe Reader. Adobe Reader is always labeled Adobe Reader X, Adobe Reader XI, and so forth. This script allows you to do this without having to find out every version that is installed throughout a network and then enter an uninstaller line for each version. You just need to enter Adobe Reader as the application name and the desired switches. It will then search the name fields in the 32 and 64 bit uninstall registry keys to find the associated GUID. Finally, it will execute an msiexec.exe /x {GUID} to uninstall that version.

This is an update to the previous post on this. I dramatically improved the code to make this function much more efficient. 

NOTE: I used Sapien's PowerShell Studio to write this script that significantly simplified the process and made it a snap to write. I highly recommend this product for all PowerShell scripters!


 <#  
      .SYNOPSIS  
           Uninstall MSI by Application Name  
        
      .DESCRIPTION  
           Here is a function that will uninstall an MSI installed application by the name of the app. You do not need to input the entire name either. For instance, say you are uninstalling all previous versions of Adobe Reader. Adobe Reader is always labeled Adobe Reader X, Adobe Reader XI, and so forth. You just need to enter Adobe Reader as the application name and the desired switches. It will then search the name fields in the 32 and 64 bit uninstall registry keys to find the associated GUID. Finally, it will execute an msiexec.exe /x {GUID} to uninstall that version.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.136  
           Created on:       3/6/2017 2:24 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         UninstallMSIByName.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Uninstall-MSIByName {  
 <#  
      .SYNOPSIS  
           Uninstall-MSIByName  
        
      .DESCRIPTION  
           Uninstalls an MSI application using the MSI file  
        
      .PARAMETER ApplicationName  
           Display Name of the application. This can be part of the name or all of it. By using the full name as displayed in Add/Remove programs, there is far less chance the function will find more than one instance.  
        
      .PARAMETER Switches  
           MSI switches to control the behavior of msiexec.exe when uninstalling the application.  
        
      .EXAMPLE  
           Uninstall-MSIByName "Adobe Reader" "/qb- /norestart"  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][String]$ApplicationName,  
           [ValidateNotNullOrEmpty()][String]$Switches  
      )  
        
      #MSIEXEC.EXE  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      #Get list of all Add/Remove Programs for 32-Bit and 64-Bit  
      $Uninstall = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ErrorAction SilentlyContinue  
      If (((Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture).OSArchitecture) -eq "64-Bit") {  
           $Uninstall += Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ErrorAction SilentlyContinue  
      }  
      #Find the registry containing the application name specified in $ApplicationName  
      $Key = $uninstall | foreach-object { Get-ItemProperty REGISTRY::$_ } | where-object { $_.DisplayName -like "*$ApplicationName*" }  
      If ($Key -ne $null) {  
           Write-Host "Uninstall"$Key.DisplayName"....." -NoNewline  
           #Define msiexec.exe parameters to use with the uninstall  
           $Parameters = "/x " + $Key.PSChildName + [char]32 + $Switches  
           #Execute the uninstall of the MSI  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
           #Return the success/failure to the display  
           If (($ErrCode -eq 0) -or ($ErrCode -eq 3010) -or ($ErrCode -eq 1605)) {  
                Write-Host "Success" -ForegroundColor Yellow  
           } else {  
                Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
           }  
      }  
 }  
   
 Clear-Host  
 Uninstall-MSIByName -ApplicationName "Cisco Jabber" -Switches "/qb- /norestart"  
   

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