Mick's IT Blogs

Mick's IT Blogs

Latest Updates

25 August 2015

Missing Bitlocker Recovery Keys Reporting Tool

Posted By: Mick Pletcher - 5:26 PM












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

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

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

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

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

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

You can download the script from here.


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

24 August 2015

Tracking Unlicensed Software using PowerShell, SCCM, and Orchestrator

Posted By: Mick Pletcher - 5:19 PM
Keeping track of systems that have unlicensed software installed can be a daunting task, especially when true up comes. We all have encountered systems that have it installed because the user called in screaming and the help desk went ahead with the install to just shut the user up. I decided that using PowerShell, SCCM, and Orchestrator would make this a much easier process.

I wrote the PowerShell script that will do the following. First comes the query for all systems with the specified software installed. The easiest way is to create the query in SCCM first that returns computer names only. Copy the WQL code for the query and paste it as the data for the $WQL variable.

Next, if you don't already have a collection with all systems that are supposed to have the software installed, then create one. I would suggest allowing only a few admins privs to add to the collection. Get the collection ID and enter that for the Get-CollectionSystems -CollectionID parameter.

The script will delete the previous file created. There are two parameters that need to be populated. Those are -Path and $OutputFile. Path is the location where you want the report file saved and OutputFile is the name of the report file. 

I setup Orchestrator to run the powershell script once a week on the SCCM server. It then sends the generated csv file in an email to all pertinent management for review. Systems popping up on the report can be dealt with immediately thereby keeping the licensing up to date on a weekly basis. 

NOTE: The PowerShell script will need to be executed on the SCCM server.

You can download the script from here.


 <#       
      .NOTES  
      ===========================================================================  
       Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.92  
       Created on:       8/21/2015 1:33 PM  
       Created by:       Mick Pletcher  
       Filename:         LicensedSoftwareVerification.ps1  
      ===========================================================================  
      .DESCRIPTION  
           This script will query SCCM for all systems with a specified   
           software installed. It will then grab all system within a specified  
           collection to compare the query with. The collection is the   
           definitive place where all necessary systems are placed that   
           require the licensed software. Any systems the query sees that are  
           not in the collection will be added to the excel report for further  
           investigation. If the system is valid, it should then be added to  
           the collection.  
 #>  
   
 param  
 (  
      [string]  
      $OutputFile = 'AdobeAcrobatReport.csv',  
      [string]  
      $Path  
 )  
   
 function ProcessTextFile {  
      If ((Test-Path -Path $OutputFile) -eq $true) {  
           Remove-Item -Path $OutputFile -Force  
      }  
 }  
   
 function Get-CollectionSystems {  
   Param([string]$CollectionID)  
   
      #Declare Local Variables  
      Set-Variable -Name System -Scope Local -Force  
      Set-Variable -Name SystemArray -Scope Local -Force  
      Set-Variable -Name Systems -Scope Local -Force  
        
   $SystemArray = @()  
      $Systems = get-cmdevice -collectionid $CollectionID | select name | Sort-Object Name  
   Foreach ($System in $Systems) {  
     $SystemArray = $SystemArray + $System.Name  
   }  
      Return $SystemArray  
        
      #Cleanup Local Variables  
      Remove-Variable -Name System -Scope Local -Force  
      Remove-Variable -Name SystemArray -Scope Local -Force  
      Remove-Variable -Name Systems -Scope Local -Force  
 }  
   
   
 cls  
 Import-Module "D:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force -Scope Global  
 Set-Location SCCMSiteCode:  
 $CollectionSystems = @()  
 $QuerySystems = @()  
 $UnlicensedSystems = @()  
 #Input the SCCM query code for the $WQL variable  
 $WQL = 'select * from SMS_R_System inner join SMS_G_System_ADD_REMOVE_PROGRAMS on SMS_G_System_ADD_REMOVE_PROGRAMS.ResourceID = SMS_R_System.ResourceId   
   
 where SMS_G_System_ADD_REMOVE_PROGRAMS.DisplayName = "Adobe Acrobat 8 Professional" or SMS_G_System_ADD_REMOVE_PROGRAMS.DisplayName = "Adobe Acrobat X Pro -   
   
 English, Fran├žais, Deutsch" or SMS_G_System_ADD_REMOVE_PROGRAMS.DisplayName = "Adobe Acrobat X Standard - English, Fran├žais, Deutsch" or   
   
 SMS_G_System_ADD_REMOVE_PROGRAMS.DisplayName = "Adobe Acrobat XI Pro" or SMS_G_System_ADD_REMOVE_PROGRAMS.DisplayName = "Adobe Acrobat XI Standard"'  
 $WMI = Get-WmiObject -Namespace Root\SMS\Site_BNA -Query $WQL  
 #Use the collectionID of the collection you use as the definitive licensing site  
 $CollectionSystems = Get-CollectionSystems -CollectionID "SCCM00024"  
 Set-Location c:  
 $OutputFile = $Path + "\" + $OutputFile  
 ProcessTextFile  
 $Output = "Computer Name"  
 Out-File -FilePath $OutputFile -InputObject $Output -Force -Encoding UTF8  
 Foreach ($Item in $WMI) {  
      $QuerySystems = $QuerySystems + $Item.SMS_R_System.Name  
 }  
 Foreach ($QuerySystem in $QuerySystems) {  
   $SystemVerified = $false  
   Foreach ($CollectionSystem in $CollectionSystems) {  
     If ($QuerySystem -eq $CollectionSystem) {  
       $SystemVerified = $true  
     }  
   }  
   If ($SystemVerified -eq $false) {  
     Out-File -FilePath $OutputFile -InputObject $QuerySystem -Force -Encoding UTF8  
   }  
 }  

21 August 2015

PowerShell Approved Verb Cheat Sheet

Posted By: Mick Pletcher - 12:39 PM





Here is a cheat sheet that is a list of all PowerShell approved verbs.

You can download cheat sheet from here

14 August 2015

SCCM Inactive Systems Report

Posted By: Mick Pletcher - 2:26 PM












I have wanted a PowerShell script that would generate an excel report listing all of the inactive systems, if they are in active directory, and the last time someone logged onto the system if it is in AD. As we all know, it is easy to overlook these systems and they can accumulate, even if the cleanup is setup in SCCM. I chose to use SCCM as the definitive report because it pulls it's initial listing of systems from AD. This script has to be executed on the SCCM server.

I have the script require two parameters: 1) OutputFile and 2) Path. The output file is the name of the CSV file and the Path is the location for the script to write the CSV file to. I have prepopulated the OutputFile parameter, but you can change that if necessary.

The next thing will be that you will need to find your collection ID of your Client Activity: Inactive collection and update that on line 40. If you do not have this collection, you will need to create a collection listing all inactive systems and use the collection ID assigned to it.

Now find out where ConfigurationManager.psd1 is located on your SCCM server and update line 94 with the full path to the module.

The SCCM server needs the PowerShell active directory module feature enabled for this script to function. It is located under RSAT in the Windows Features.

Finally, you will need to execute this script on the SCCM server. I have not found any way around this. I have this script setup in Microsoft Orchestrator that uses PSEXEC.EXE to execute the PowerShell script locally on the SCCM server on a weekly basis. It works flawlessly. Again, this script is running in my company's environment, which will largely differ from others. You will need to modify this script to get it to work in your environment.

You can download the script from here.


1:  <#  
2:            ===========================================================================  
3:            Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.91  
4:            Created on:       8/13/2015 1:36 PM  
5:            Created by:       Mick Pletcher  
6:            Organization:  
7:            Filename:         InactiveSCCMSystemsReport.ps1  
8:            Description:      This script will retrieve the SCCM inactive systems  
9:                              collection and search active directory to see if it   
10:                             exists there. If so, it will retrieve the last   
11:                             logon date and generate a UTF-8 formatted csv file.  
12:    
13:                             The powershell active directory module will need to be  
14:                             enabled on the SCCM server in order for this script to   
15:                             work correctly. This script will also need to be executed  
16:                             on the SCCM server. You will also need to find the location  
17:                             of ConfigurationManager.psd1 module to import.   
18:            ===========================================================================  
19:  #>  
20:  param  
21:  (  
22:       [string]  
23:       $OutputFile = 'InactiveSCCMSystemsReport.csv',  
24:       [string]  
25:       $Path  
26:  )  
27:  Import-Module ActiveDirectory  
28:    
29:    
30:  function ProcessTextFile {  
31:       If ((Test-Path -Path $OutputFile) -eq $true) {  
32:            Remove-Item -Path $OutputFile -Force  
33:       }  
34:  }  
35:    
36:  function Get-SCCMInactiveSystems {  
37:       #Declare Local Variables  
38:       Set-Variable -Name Systems -Scope Local -Force  
39:         
40:       $Systems = get-cmdevice -collectionid "BNA00093" | select name | Sort-Object Name  
41:       Return $Systems  
42:         
43:       #Cleanup Local Variables  
44:       Remove-Variable -Name Systems -Scope Local -Force  
45:  }  
46:    
47:  function Find-SCCMInactiveSystemInAD {  
48:       param ([string]  
49:            $System)  
50:         
51:       #Declare Local Variables  
52:       Set-Variable -Name AD -Scope Local -Force  
53:       $ErrorActionPreference = 'SilentlyContinue'  
54:       $AD = Get-ADComputer $System  
55:       $ErrorActionPreference = 'Continue'  
56:       if ($AD -ne $null) {  
57:            Return "X"  
58:       } else {  
59:            Return " "       
60:       }  
61:         
62:       #Cleanup Local Variables  
63:       Remove-Variable -Name AD -Scope Local -Force  
64:  }  
65:    
66:  function Get-LastLogonDate {  
67:       param ([string]  
68:            $System)  
69:         
70:       #Declare Local Variables  
71:       Set-Variable -Name AD -Scope Local -Force  
72:         
73:       $AD = Get-ADComputer $System -ErrorAction SilentlyContinue  
74:       $AD = $AD.SamAccountName  
75:       $AD = $AD.Substring(0, $AD.Length - 1)  
76:       $AD = Get-ADComputer -Identity $AD -Properties *  
77:       $AD = $AD.LastLogonDate  
78:       Return $AD  
79:              
80:       #Cleanup Local Variables  
81:       Remove-Variable -Name AD -Scope Local -Force  
82:  }  
83:    
84:  #Declare Variables  
85:  Set-Variable -Name ADEntry -Scope Local -Force  
86:  Set-Variable -Name Counter -Value 1 -Scope Local -Force  
87:  Set-Variable -Name LastLogon -Scope Local -Force  
88:  Set-Variable -Name Output -Scope Local -Force  
89:  Set-Variable -Name SCCMInactiveSystems -Scope Local -Force  
90:  Set-Variable -Name System -Scope Local -Force  
91:    
92:  cls  
93:  Import-Module -Name ActiveDirectory  
94:  Import-Module "D:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Force -Scope Global  
95:  Set-Location BNA:  
96:  $SCCMInactiveSystems = Get-SCCMInactiveSystems  
97:  Set-Location c:  
98:  $OutputFile = $Path + "\" + $OutputFile  
99:  ProcessTextFile  
100:  $Output = "Computer Name" + [char]44+"Active Directory"+[char]44+"Last Logon"  
101:  Out-File -FilePath $OutputFile -InputObject $Output -Force -Encoding UTF8  
102:  foreach ($System in $SCCMInactiveSystems) {  
103:       cls  
104:       $Output = "Processing "+$System.Name+" -- "+$Counter+" of "+$SCCMInactiveSystems.Count  
105:       Write-Host $Output  
106:       $Counter++  
107:       $ADEntry = Find-SCCMInactiveSystemInAD -System $System.Name  
108:       If ($ADEntry -ne " ") {  
109:            $LastLogon = Get-LastLogonDate -System $System.Name  
110:       }  
111:       $Output = $System.Name+[char]44+$ADEntry+[char]44+$LastLogon  
112:       Out-File -FilePath $Global:OutputFile -InputObject $Output -Append -Force -Encoding UTF8  
113:       $ADEntry = $null  
114:       $LastLogon = $null  
115:       $Output = $null  
116:  }  
117:    
118:  #Cleanup Variables  
119:  Remove-Variable -Name ADEntry -Scope Local -Force  
120:  Remove-Variable -Name Counter -Scope Local -Force  
121:  Remove-Variable -Name LastLogon -Scope Local -Force  
122:  Remove-Variable -Name Output -Scope Local -Force  
123:  Remove-Variable -Name SCCMInactiveSystems -Scope Local -Force  
124:  Remove-Variable -Name System -Scope Local -Force  
125:    

10 August 2015

Windows Updates Reporting Tool

Posted By: Mick Pletcher - 5:11 PM





My firm uses MDT to build all of our computers. The windows updates are setup as packages so they can be applied to the OS before it is laid down on the PC. MDT will then download any new updates and apply them to the PC after the OS has been laid down. In order to keep track of new updates needing to be integrated as a package, I have written the following PowerShell script that will generate a CSV file listing all of the updates installed during the build and in the exact order they were installed. This gives a clean report of the updates instead of having to dig through the ZTIWindowsUpdate.log, which is what this report is generated from. I have integrated this script into the task sequence to execute immediately after the task Windows Update (Post-Application Installation). There are two parameters that need to be defined when calling the script. These are:

  1. OutputFile - give the name of the file you desire such as BaseBuildUpdatesReport.csv
  2. Path - Defines the location where you want the OutputFile to be written to
Here is an example of a command line:
  • powershell.exe -executionpolicy bypass -file WindowsUpdatesReport.ps1 -OutputFile BaseBuild.csv -Path \\NetworkLocation\Directory
Here is a screenshot setting it up as a task sequence in MDT:

You can download the code from here.




 <#       
      .NOTES  
      ===========================================================================  
       Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.90  
       Created on:       8/7/2015 9:54 AM  
       Created by:       Mick Pletcher  
       Filename:        WindowsUpdatesReport.ps1  
      ===========================================================================  
      .DESCRIPTION  
           This script will extract the list of windows updates installed   
           during an MDT installation.  
      .EXAMPLE  
           powershell.exe -executionpolicy bypass -file WindowsUpdatesReport.ps1 -OutputFile BaseBuild.csv -Path \\NetworkLocation\Directory  
 #>  
   
 param ([string]$OutputFile, [string]$Path)  
   
 function Get-RelativePath {  
      #Declare Local Variables  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $RelativePath  
        
      #Cleanup Local Variables  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function ProcessTextFile {  
      #Declare Local Variables   
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      If ((Test-Path -Path $RelativePath$OutputFile) -eq $true) {  
           Remove-Item -Path $RelativePath$OutputFile -Force  
      }  
        
      #Cleanup Local Variables   
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function Get-Updates {  
      #Declare Local Variables  
      Set-Variable -Name File -Scope Local -Force  
      Set-Variable -Name Line -Scope Local -Force  
      Set-Variable -Name LogFile -Scope Local -Value $env:SystemDrive"\MININT\SMSOSD\OSDLOGS\ZTIWindowsUpdate.log" -Force  
      Set-Variable -Name Name -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $OutputArray = @()  
      $RelativePath = Get-RelativePath  
      $File = Get-Content -Path $LogFile  
      $Global:OutputFile = $RelativePath + $Global:OutputFile  
      $Output = "KB Article" + "," + "Description"  
      Out-File -FilePath $Global:OutputFile -InputObject $Output -Append -Force -Encoding UTF8  
      If ($File -ne $null) {  
           foreach ($Line in $File) {  
                Set-Variable -Name KB -Scope Local -Force  
                If ($Line -like "*INSTALL - *") {  
                     $Name = $Line  
                     $Name = $Name -replace 'x64-based', 'x64 based'  
                     $Name = $Name -replace '32-Bit', '32 Bit'  
                     $Name = $Name.split('-')  
                     If ($Name[7] -like "*Definition*") {  
                          $KB = $Name[7]  
                          $KB = $KB.Trim()  
                          $KB = $KB.split(' ')  
                          $KB = $KB.Trim()  
                          [string]$KB = $KB[0]  
                          $Name = $Name[6]  
                          $Name = $Name.Trim()  
                     } else {  
                          $KB = $Name[6]  
                          $KB = $KB.split('(')  
                          $KB = $KB.split(')')  
                          $KB = $KB.Trim()  
                          $KB = $KB[1]  
                          $Name = $Name[6]  
                          $Name = $Name.split('(')  
                          $Name = $Name[0]  
                          $Name = $Name.Trim()  
                     }  
                     $Output = $KB + "," + $Name  
                     $OutputArray = $OutputArray + $Output  
                     Remove-Variable -Name KB -Scope Local -Force  
                }  
           }  
           $Line = $null  
           $OutputArray = $OutputArray | select -Unique  
           foreach ($Line in $OutputArray) {  
                Out-File -FilePath $Global:OutputFile -InputObject $Line -Append -Force -Encoding UTF8  
           }  
      } else {  
           $Output = "No User Input Properties Exist"  
           Write-Host $Output  
           Out-File -FilePath $Global:OutputFile -InputObject $Output -Append -Force -Encoding UTF8  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name File -Scope Local -Force  
      Remove-Variable -Name KB -Scope Local -Force  
      Remove-Variable -Name Line -Scope Local -Force  
      Remove-Variable -Name Name -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 cls  
 ProcessTextFile  
 Get-Updates  
   

07 August 2015

SCCM Client Installer

Posted By: Mick Pletcher - 10:53 AM





This PowerShell script will uninstall previous versions of SCCM client, run the CCMClean to make sure all instances of the client are gone off of the machine, and finally install the SCCM client. The script will not complete until the installation of the SCCM client is completed. I used this when upgrading from 2007 to 2012 and also have this integrated into the build. I pulled the functions from my installation module and put them into this script so that it can be a standalone. One thing you will need to do, if you want to include the ccmclean.exe, is to download it from here.

You can download the script below from here.


 <#  
 .SYNOPSIS  
   SCCM 2012 R2 Client  
 .DESCRIPTION  
   Install SCCM 2012 R2 Client  
 .EXAMPLE  
   powershell.exe -executionpolicy bypass -file install.ps1  
 #>  
   
 #Declare Global Variables  
 Set-Variable -Name RelativePath -Scope Global -Force  
 Set-Variable -Name Title -Scope Global -Force  
   
 Function InitializeVariables {  
      $Global:RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent)+"\"   
      $Global:Title = "SCCM 2012 R2 Client"  
 }  
   
 Function Install-EXE {  
      <#  
      .SYNOPSIS  
           Install-EXE  
      .DESCRIPTION  
           Installs an EXE file  
      .EXAMPLE  
           Install-EXE -DisplayName "Microsoft Office 2013" -Executable "c:\temp\install.exe" -Switches "/passive /norestart"  
      #>  
        
      Param ([String]  
           $DisplayName,  
           [String]  
           $Executable,  
           [String]  
           $Switches)  
        
      Write-Host "Install"$DisplayName"....." -NoNewline  
      If ((Test-Path $Executable) -eq $true) {  
           $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -Passthru).ExitCode  
      } else {  
           $ErrCode = 1  
      }  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
 }  
   
 Function Install-MSP {  
      <#  
      .SYNOPSIS  
           Install-MSP  
      .DESCRIPTION  
           Installs an MSP patch  
      .EXAMPLE  
           Install-MSP -DisplayName "KB977203" -MSP $Global:RelativePath"i386\sccm2007ac-sp2-kb977203-x86.msp" -Switches "/qb- /norestart"  
      #>  
        
      Param ([String]  
           $DisplayName,  
           [String]  
           $MSP,  
           [String]  
           $Switches)  
        
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      $Parameters = "/p " + [char]34 + $MSP + [char]34 + [char]32 + $Switches  
      Write-Host "Install"$DisplayName"....." -NoNewline  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
 }  
   
 Function Set-ConsoleTitle {  
      <#  
      .SYNOPSIS  
           Set-ConsoleTitle  
      .DESCRIPTION  
           Renames the PowerShell console window  
      .EXAMPLE  
           Set-ConsoleTitle -Title "Test"  
      #>  
        
      Param ([String]  
           $Title)  
      $host.ui.RawUI.WindowTitle = $Title  
 }  
   
 Function Stop-Task {  
      <#  
      .SYNOPSIS  
           Stop-Task  
      .DESCRIPTION  
           Kills a designated Task  
      .EXAMPLE  
           Stop-Task -Process "outlook"  
      #>  
        
      Param ([String]  
           $Process)  
        
      $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
      Write-Host "Killing"$Process"....." -NoNewline  
      If ($Proc -ne $null) {  
           Do {  
                $ProcName = $Process + ".exe"  
                $Temp = taskkill /F /IM $ProcName  
                Start-Sleep -Seconds 2  
                $Proc = $null  
                $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
           } While ($Proc -ne $null)  
           Write-Host "Closed" -ForegroundColor Yellow  
      } else {  
           Write-Host "Already Closed" -ForegroundColor Green  
      }  
 }  
   
 Function Uninstall-EXE {  
      <#  
      .SYNOPSIS  
           Uninstall-EXE  
      .DESCRIPTION  
           Uninstalls an EXE file  
      .EXAMPLE  
           Uninstall-EXE -DisplayName "Microsoft Office 2013" -Executable "c:\temp\setup.exe" -Switches "/uninstall /passive /norestart"  
      #>  
        
      Param ([String]  
           $DisplayName,  
           [String]  
           $Executable,  
           [String]  
           $Switches)  
        
      Write-Host "Uninstall"$DisplayName"....." -NoNewline  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Switches -Wait -Passthru).ExitCode  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
 }  
   
 Function Uninstall-MSIByGUID {  
      <#  
      .SYNOPSIS  
           Uninstall-MSIByGUID  
      .DESCRIPTION  
           Uninstalls an MSI application using the GUID  
      .EXAMPLE  
           Uninstall-MSIByGUID -DisplayName "Workshare Professional" -GUID "{8686EC18-6282-4AA9-92AC-2865B972E244}" -Switches "/qb- /norestart"  
      #>  
        
      Param ([String]  
           $DisplayName,  
           [String]  
           $GUID,  
           [String]  
           $Switches)  
        
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      $Parameters = "/x " + $GUID + [char]32 + $Switches  
      Write-Host "Uninstall"$DisplayName"....." -NoNewline  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
      If (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } elseIf ($ErrCode -eq 1605) {  
           Write-Host "Not Present" -ForegroundColor Green  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
 }  
   
 Function Uninstall-MSIByName {  
      <#  
      .SYNOPSIS  
           Uninstall-MSIByName  
      .DESCRIPTION  
           Uninstalls an MSI application using the MSI file  
      .EXAMPLE  
           Uninstall-MSIByName -ApplicationName "Adobe Reader" -Switches "/qb- /norestart"  
      #>  
        
      Param ([String]  
           $ApplicationName,  
           [String]  
           $Switches)  
        
      $Uninstall = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $Uninstall += Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $SearchName = "*" + $ApplicationName + "*"  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      Foreach ($Key in $Uninstall) {  
           $TempKey = $Key.Name -split "\\"  
           If ($TempKey[002] -eq "Microsoft") {  
                $Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           } else {  
                $Key = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           }  
           If ((Test-Path $Key) -eq $true) {  
                $KeyName = Get-ItemProperty -Path $Key  
                If ($KeyName.DisplayName -like $SearchName) {  
                     $TempKey = $KeyName.UninstallString -split " "  
                     If ($TempKey[0] -eq "MsiExec.exe") {  
                          Write-Host "Uninstall"$KeyName.DisplayName"....." -NoNewline  
                          $Parameters = "/x " + $KeyName.PSChildName + [char]32 + $Switches  
                          $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
                          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  
                          }  
                     }  
                }  
           }  
      }  
 }  
   
 Function Wait-ProcessEnd {  
      <#  
      .SYNOPSIS  
           Wait-Process  
      .DESCRIPTION  
           Waits for a Process to end before continuing.  
      .EXAMPLE  
           Wait-Process -Process "explorer"  
      #>  
        
      Param ([String]  
           $Process)  
        
      Write-Host "Waiting for"$Process" to end....." -NoNewline  
      $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
      If ($Proc -ne $null) {  
           Do {  
                Start-Sleep -Seconds 5  
                $Proc = Get-Process $Process -ErrorAction SilentlyContinue  
           } While ($Proc -ne $null)  
           Write-Host "Ended" -ForegroundColor Yellow  
      } else {  
           Write-Host "Process Already Ended" -ForegroundColor Yellow  
      }  
 }  
   
 cls  
 InitializeVariables  
 Set-ConsoleTitle -Title $Global:Title  
 Stop-Task -Process "msiexec"  
 Uninstall-MSIByName -ApplicationName "Configuration Manager Client" -Switches "/qb- /norestart"  
 Stop-Task -Process "msiexec"  
 Uninstall-MSIByGUID -DisplayName "SCCM 2007 Client" -GUID "{2609EDF1-34C4-4B03-B634-55F3B3BC4931}" -Switches "/qb- /norestart"  
 Stop-Task -Process "msiexec"  
 Uninstall-MSIByGUID -DisplayName "SCCM 2012 Client" -GUID "{BFDADC41-FDCD-4B9C-B446-8A818D01BEA3}" -Switches "/qb- /norestart"  
 Stop-Task -Process "msiexec"  
 Uninstall-EXE -DisplayName "CCMClean" -Executable $Global:RelativePath"ccmclean.exe" -Switches "/all /logdir:%windir%\logs /removehistory /q"  
 Stop-Task -Process "msiexec"  
 Install-EXE -DisplayName "SCCM 2012 R2 Client" -Executable $Global:RelativePath"ccmsetup.exe" -Switches "/mp:sccm.acme.int SMSSITECODE=BNA"  
 Wait-ProcessEnd -Process "CCMSETUP"  
 Stop-Task -Process "msiexec"  
   
 #Cleanup Global Variables  
 Remove-Variable -Name RelativePath -Scope Global -Force  
 Remove-Variable -Name Title -Scope Global -Force  

27 July 2015

MSI Analysis Reporting Tool

Posted By: Mick Pletcher - 4:28 PM





Here is a tool I have written that generates a report on all of the variables used for each entry during an MSI installation. I wrote this script to make customizing the installation of MSI files much easier and quicker. The way this script works is that it will install the MSI file and then come back and uninstall it. It will produce a verbose text file which the script will read from to produce the report.

To use this script, just copy it to the same location as the MSI and execute it. If there is more than one MSI within the directory, you will be prompted on which one to execute. The script will then pop up a blank notepad file. This is to populate with all of your test responses during the setup. For each box you type a text entry into, copy and paste that into the notepad file making a single line entry for each text box. 

If you see a folder window, click change and then copy the directory listed. This will then report the variable associated with the folder.

For radio button boxes, you can enter part of the name of the button, or the entire name. Some radio buttons have the ampersand within the name. It is does, this will not detect it, therefor you will need to enter part of the name. 

The features will be automatically detected, so there is no need to enter those into the text file. The selected features will be read from the log file.

In the end, the report will be published on the screen and to the report.txt file. The following data will be in the report: Product Name, Product Code, MSI Filename, Properties, Features, and Buttons.

NOTE: If you are going to execute the script from a command prompt, make sure the command prompt is also residing in the same directory as the script.

Here is an example report.txt file of packaging InterAction



This was the text input file used during the installation that helped generate the above report:
You can download the script from here.


 <#  
      .SYNOPSIS  
           Generate a report on available values in MSI  
        
      .DESCRIPTION  
           This script will install an MSI file and give a report on the variables used within the MSI  
           for each user input field.   
        
      .EXAMPLE  
           powershell.exe -executionpolicy bypass -file MSIAnalyzer.ps1  
   
      .Author  
           Mick Pletcher  
   
      .Date  
           22 July 2015  
        
      .NOTES  
           Additional information about the file.  
 #>  
   
 #Declare Global Variables  
 Set-Variable -Name ApplicationName -Scope Global -Force  
 Set-Variable -Name MSI -Scope Global -Force  
 Set-Variable -Name ProductCode -Scope Global -Force  
   
 function Get-RelativePath {  
      #Declare Local Variables  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $RelativePath  
        
      #Cleanup Local Variables  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function Get-Architecture {  
      #Declare Local Variables  
      Set-Variable -Name Architecture -Scope Local -Force  
        
      $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
      $Architecture = $Global:Architecture.OSArchitecture  
      Return $Architecture  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Architecture -Scope Local -Force  
 }  
   
 function ProcessTextFiles {  
      #Declare Local Variables  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      If ((Test-Path -Path $RelativePath"Installer.log") -eq $true) {  
           Remove-Item -Path $RelativePath"Installer.log" -Force  
      }  
      If ((Test-Path -Path $RelativePath"UserInput.txt") -eq $true) {  
           Remove-Item -Path $RelativePath"UserInput.txt" -Force  
      }  
      If ((Test-Path -Path $RelativePath"Report.txt") -eq $true) {  
           Remove-Item -Path $RelativePath"Report.txt" -Force  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function Get-MSIFile {  
      #Declare Local Variables  
      Set-Variable -Name FileCount -Value 1 -Scope Local -Force  
      Set-Variable -Name MSIFile -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $MSIFile = Get-ChildItem $RelativePath -Filter *.msi  
      Write-Host $MSIFile.Count  
      If ($MSIFile.Count -eq 1) {  
           Return $MSIFile  
      } else {  
           Do {  
                Clear-Host  
                $FileCount = 1  
                Write-Host "Select MSI to process:"  
                foreach ($MSI in $MSIFile) {  
                     Write-Host $FileCount" - "$MSI  
                     $FileCount++  
                }  
                Write-Host  
                Write-Host "Selection:"  
                [int]$input = Read-Host  
           } while (($input -eq $null) -or ($input -eq "") -or ($input -gt $MSIFile.Count) -or (!($input -as [int] -is [int])))  
           $input = $input - 1  
           $MSIFile = $MSIFile[$input]  
           $global:MSI = $RelativePath + $MSIFile  
      }  
                  
      #Cleanup Local Variables  
      Remove-Variable -Name FileCount -Scope Local -Force  
      Remove-Variable -Name MSIFile -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
 }  
   
 function Get-MSIFileInfo {  
      param (  
           [parameter(Mandatory = $true)][IO.FileInfo]  
           $Path,  
           [parameter(Mandatory = $true)][ValidateSet("ProductCode", "ProductVersion", "ProductName")][string]  
           $Property  
      )  
        
      #Declare Local Variables  
      Set-Variable -Name MSIDatabase -Scope Local -Force  
      Set-Variable -Name Query -Scope Local -Force  
      Set-Variable -Name Record -Scope Local -Force  
      Set-Variable -Name Value -Scope Local -Force  
      Set-Variable -Name View -Scope Local -Force  
      Set-Variable -Name WindowsInstaller -Scope Local -Force  
        
      try {  
           $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer  
           $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($Path.FullName, 0))  
           $Query = "SELECT Value FROM Property WHERE Property = '$($Property)'"  
           $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)  
           $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)  
           return $Value  
      } catch {  
           Write-Output $_.Exception.Message  
      }  
        
      #Declare Local Variables  
      Remove-Variable -Name MSIDatabase -Scope Local -Force  
      Remove-Variable -Name Query -Scope Local -Force  
      Remove-Variable -Name Record -Scope Local -Force  
      Remove-Variable -Name Value -Scope Local -Force  
      Remove-Variable -Name View -Scope Local -Force  
      Remove-Variable -Name WindowsInstaller -Scope Local -Force  
 }  
   
 function New-UserInputFile {  
      #Declare Local Variables  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      If ((Test-Path -Path $RelativePath"UserInput.txt") -eq $true) {  
           Remove-Item -Path $RelativePath"UserInput.txt" -Force  
      }  
      Write-Host "Creating UserInput.txt File....." -NoNewline  
      $ErrCode = New-Item -Path $RelativePath"UserInput.txt" -Type File -Force  
      If ((Test-Path -Path $RelativePath"UserInput.txt") -eq $true) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
        
 }  
   
 function Install-MSI {  
      #Declare Local Variables  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name Executable -Scope Local -Force  
      Set-Variable -Name MSI -Scope Local -Force  
      Set-Variable -Name Parameters -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name Switches -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $Global:ApplicationName = Get-MSIFileInfo -Path $global:MSI -Property 'ProductName'  
      $Global:ProductCode = Get-MSIFileInfo -Path $global:MSI -Property 'ProductCode'  
      If ((Test-Path -Path $RelativePath"Installer.log") -eq $true) {  
           Remove-Item -Path $RelativePath"Installer.log" -Force  
      }  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      $Switches = "/qb- /norestart"  
      $Parameters = "/x" + [char]32 + $Global:ProductCode + [char]32 + $Switches  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
      Write-Host "Uninstalling"$Global:ApplicationName"....." -NoNewline  
      if (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
      $Notepad = $env:windir + "\notepad.exe"  
      $Parameters = $RelativePath + "UserInput.txt"  
      $ErrCode = (Start-Process -FilePath $Notepad -ArgumentList $Parameters -PassThru).ExitCode  
      $Switches = "/norestart"  
      $Parameters = "/i " + [char]34 + $Global:MSI + [char]34 + [char]32 + $Switches + [char]32 + "/lvx " + [char]34 + $RelativePath + "Installer.log" + [char]34  
      Write-Host $Parameters  
      Write-Host "Installing"$Global:ApplicationName"....." -NoNewline  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
      if (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
      Write-Host "Creating Log File....." -NoNewline  
      If ((Test-Path $RelativePath"Installer.log") -eq $true) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed" -ForegroundColor Red  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name Executable -Scope Local -Force  
      Remove-Variable -Name MSI -Scope Local -Force  
      Remove-Variable -Name Parameters -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name Switches -Scope Local -Force  
 }  
   
 function Uninstall-MSI {  
      #Declare Local Variables  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name Executable -Scope Local -Force  
      Set-Variable -Name Line -Scope Local -Force  
      Set-Variable -Name LogFile -Scope Local -Force  
      Set-Variable -Name MSI -Scope Local -Force  
      Set-Variable -Name Parameters -Scope Local -Force  
      Set-Variable -Name Process -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name Switches -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $Process = Get-Process -Name notepad -ErrorAction SilentlyContinue  
      If ($Process -ne $null) {  
           Stop-Process -Name notepad -ErrorAction SilentlyContinue -Force  
      }  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      $Switches = "/qb- /norestart"  
      $Parameters = "/x" + [char]32 + $Global:ProductCode + [char]32 + $Switches  
      Write-Host "Uninstalling"$Global:ApplicationName"....." -NoNewline  
      $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
      if (($ErrCode -eq 0) -or ($ErrCode -eq 3010)) {  
           Write-Host "Success" -ForegroundColor Yellow  
      } else {  
           Write-Host "Failed with error code "$ErrCode -ForegroundColor Red  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name Executable -Scope Local -Force  
      Remove-Variable -Name Line -Scope Local -Force  
      Remove-Variable -Name LogFile -Scope Local -Force  
      Remove-Variable -Name MSI -Scope Local -Force  
      Remove-Variable -Name Parameters -Scope Local -Force  
      Remove-Variable -Name Process -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name Switches -Scope Local -Force  
 }  
   
 function Get-ProductName {  
      #Declare Local Variables  
      Set-Variable -Name Database -Scope Local -Force  
      Set-Variable -Name MSIFileName -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
      Set-Variable -Name PropertyName -Scope Local -Force  
      Set-Variable -Name PropertyValue -Scope Local -Force  
      Set-Variable -Name Record -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name View -Scope Local -Force  
      Set-Variable -Name WindowsInstaller -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $MSIFileName = $global:MSI  
      $OutputFile = $RelativePath + "Report.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "Product Name"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "------------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $WindowsInstaller = New-Object -com WindowsInstaller.Installer  
      $Database = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($MSIFileName, 0))  
      $View = $Database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $Database, ("SELECT * FROM Property"))  
      $View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)  
      $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
      while ($Record -ne $Null) {  
           $PropertyName = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 1)  
           [string]$PropertyValue = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 2)  
           IF ($PropertyName -like "*ProductName*") {  
                $Output = $PropertyValue  
                Write-Host $Output  
                Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
           }  
           $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Database -Scope Local -Force  
      Remove-Variable -Name MSIFileName -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
      Remove-Variable -Name PropertyName -Scope Local -Force  
      Remove-Variable -Name PropertyValue -Scope Local -Force  
      Remove-Variable -Name Record -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name View -Scope Local -Force  
      Remove-Variable -Name WindowsInstaller -Scope Local -Force  
 }  
   
 function Get-ProductCode {  
      #Declare Local Variables  
      Set-Variable -Name Database -Scope Local -Force  
      Set-Variable -Name MSIFileName -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
      Set-Variable -Name PropertyName -Scope Local -Force  
      Set-Variable -Name PropertyValue -Scope Local -Force  
      Set-Variable -Name Record -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name View -Scope Local -Force  
      Set-Variable -Name WindowsInstaller -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $MSIFileName = $global:MSI  
      $OutputFile = $RelativePath + "Report.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "Product Code"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "------------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $WindowsInstaller = New-Object -com WindowsInstaller.Installer  
      $Database = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($MSIFileName, 0))  
      $View = $Database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $Database, ("SELECT * FROM Property"))  
      $View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)  
      $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
      while ($Record -ne $Null) {  
           $PropertyName = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 1)  
           [string]$PropertyValue = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 2)  
           IF ($PropertyName -like "*ProductCode*") {  
                $Output = $PropertyValue  
                Write-Host $Output  
                Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
           }  
           $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Database -Scope Local -Force  
      Remove-Variable -Name MSIFileName -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
      Remove-Variable -Name PropertyName -Scope Local -Force  
      Remove-Variable -Name PropertyValue -Scope Local -Force  
      Remove-Variable -Name Record -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name View -Scope Local -Force  
      Remove-Variable -Name WindowsInstaller -Scope Local -Force  
 }  
   
 function Get-MSIFileName {  
      #Declare Local Variables  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
        
      $OutputFile = $RelativePath + "Report.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "MSI Filename"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "------------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = $global:MSI  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
        
 }  
   
 function Get-Properties {  
      #Declare Local Variables  
      Set-Variable -Name Entries -Scope Local -Force  
      Set-Variable -Name Entry -Scope Local -Force  
      Set-Variable -Name File -Scope Local -Force  
      Set-Variable -Name FormattedEntry -Scope Local -Force  
      Set-Variable -Name Line -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
      Set-Variable -Name Position -Scope Local -Force  
      Set-Variable -Name Property -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name Value -Scope Local -Force  
        
      $OutputArray = @()  
      $RelativePath = Get-RelativePath  
      $File = Get-Content -Path $RelativePath"Installer.log"  
      $OutputFile = $RelativePath+"Report.txt"  
      $Entries = Get-Content -Path $RelativePath"UserInput.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "Properties"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "----------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      If ($Entries -ne $null) {  
           foreach ($Line in $File) {  
                If ($Line -like "*PROPERTY CHANGE: Adding*") {  
                     foreach ($Entry in $Entries) {  
                          $FormattedEntry = [char]42 + [char]39 + $Entry + [char]39 + [char]42  
                          If ($Line -like $FormattedEntry) {  
                               $Property = $Line  
                               $Value = $Line  
                               $Property = $Property.split(':')[-1]  
                               If ($Property[0] -eq "\") {  
                                    $Property = $Line  
                                    $Property = $Property.split(':')[-2]  
                               }  
                               $Property = $Property.Trim()  
                               $Property = $Property.Trim("Adding")  
                               $Property = $Property.Trim()  
                               $Property = $Property.Trim(" ")  
                               $Position = $Property.IndexOf(" ")  
                               If ($Property -notlike "*:\*") {  
                                    $Property = $Property.Substring(0, $Position)  
                               }  
                               $Output = $Property + ": " + $Entry  
                               $OutputArray += $Output  
                          }  
                     }  
                }  
           }  
           $OutputArray = $OutputArray | select -Unique  
           $OutputArray = $OutputArray | Sort  
           foreach ($Item in $OutputArray) {  
                Write-Host $Item  
                If ($Item -ne $null) {  
                     Out-File -FilePath $OutputFile -InputObject $Item -Append -Force  
                }  
           }  
      } else {  
           $Output = "No User Input Properties Exist"  
           Write-Host $Output  
           Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Entries -Scope Local -Force  
      Remove-Variable -Name Entry -Scope Local -Force  
      Remove-Variable -Name File -Scope Local -Force  
      Remove-Variable -Name FormattedEntry -Scope Local -Force  
      Remove-Variable -Name Line -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
      Remove-Variable -Name Position -Scope Local -Force  
      Remove-Variable -Name Property -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name Value -Scope Local -Force  
 }  
   
 function Get-Features {  
      #Declare Local Variables  
      Set-Variable -Name Entries -Scope Local -Force  
      Set-Variable -Name File -Scope Local -Force  
      Set-Variable -Name Line -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name Value -Scope Local -Force  
      Set-Variable -Name Values -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $OutputFile = $RelativePath + "Report.txt"  
      $Entries = Get-Content -Path $RelativePath"UserInput.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "Features"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "--------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      If ($Entries -ne $null) {  
           $File = Get-Content -Path $global:RelativePath"Installer.log"  
           foreach ($Line in $File) {  
                If ($Line -like "*ADDLOCAL*") {  
                     $Value = $Line.split(' ')[-1]  
                     $Value = $Value -replace '''', ''  
                     $Value = $Value.SubString(0, $Value.Length - 1)  
                     $Values = $Value.Split(",")  
                     foreach ($Value in $Values) {  
                          Write-Host $Value  
                          Out-File -FilePath $OutputFile -InputObject $Value -Append -Force  
                     }  
                }  
           }  
      } else {  
           $Output = "No User Input Features Exist"  
           Write-Host $Output  
           Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Entries -Scope Local -Force  
      Remove-Variable -Name File -Scope Local -Force  
      Remove-Variable -Name Line -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name Value -Scope Local -Force  
      Remove-Variable -Name Values -Scope Local -Force  
 }  
   
 function Get-Buttons {  
      #Declare Local Variables  
      Set-Variable -Name Database -Scope Local -Force  
      Set-Variable -Name Entries -Scope Local -Force  
      Set-Variable -Name Entry -Scope Local -Force  
      Set-Variable -Name MSIFileName -Scope Local -Force  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name OutputFile -Scope Local -Force  
      Set-Variable -Name PropertyName -Scope Local -Force  
      Set-Variable -Name PropertyValue -Scope Local -Force  
      Set-Variable -Name Record -Scope Local -Force  
      Set-Variable -Name RelativePath -Scope Local -Force  
      Set-Variable -Name View -Scope Local -Force  
      Set-Variable -Name WindowsInstaller -Scope Local -Force  
        
      $RelativePath = Get-RelativePath  
      $MSIFileName = $global:MSI  
      $Entries = Get-Content -Path $RelativePath"UserInput.txt"  
      $OutputFile = $RelativePath + "Report.txt"  
      $Output = [char]13  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "Buttons"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      $Output = "--------"  
      Write-Host $Output  
      Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
      foreach ($Entry in $Entries) {  
           $WindowsInstaller = New-Object -com WindowsInstaller.Installer  
           $Database = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $WindowsInstaller, @($MSIFileName, 0))  
           $View = $Database.GetType().InvokeMember("OpenView", "InvokeMethod", $Null, $Database, ("SELECT * FROM RadioButton"))  
           $View.GetType().InvokeMember("Execute", "InvokeMethod", $Null, $View, $Null)  
           $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
           $Entry = "*" + $Entry + "*"  
           while ($Record -ne $Null) {  
                $PropertyName = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 1)  
                if (-not ($PropertyName -cmatch "[a-z]")) {  
                     [string]$PropertyValue = $Record.GetType().InvokeMember("StringData", "GetProperty", $Null, $Record, 8)  
                     IF ($PropertyValue -like $Entry) {  
                          $Output = $PropertyName + " = " + $PropertyValue  
                          Write-Host $Output  
                          Out-File -FilePath $OutputFile -InputObject $Output -Append -Force  
                          #Write-Host ($PropertyName + " = " + $PropertyValue)  
                     }  
                }  
                $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $Null, $View, $Null)  
           }  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name Database -Scope Local -Force  
      Remove-Variable -Name Entries -Scope Local -Force  
      Remove-Variable -Name Entry -Scope Local -Force  
      Remove-Variable -Name MSIFileName -Scope Local -Force  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name OutputFile -Scope Local -Force  
      Remove-Variable -Name PropertyName -Scope Local -Force  
      Remove-Variable -Name PropertyValue -Scope Local -Force  
      Remove-Variable -Name Record -Scope Local -Force  
      Remove-Variable -Name RelativePath -Scope Local -Force  
      Remove-Variable -Name View -Scope Local -Force  
      Remove-Variable -Name WindowsInstaller -Scope Local -Force  
 }  
   
 Clear-Host  
 ProcessTextFiles  
 Get-MSIFile  
 New-UserInputFile  
 Install-MSI  
 Uninstall-MSI  
 Get-ProductName  
 Get-ProductCode  
 Get-MSIFileName  
 Get-Properties  
 Get-Features  
 Get-Buttons  
   
 #Cleanup Global Variables  
 Remove-Variable -Name ApplicationName -Scope Global -Force  
 Remove-Variable -Name MSI -Scope Global -Force  
 Remove-Variable -Name ProductCode -Scope Global -Force  

16 July 2015

Running Programs as an Application Installation in SCCM 2012

Posted By: Mick Pletcher - 10:16 AM
Recently, I had to deploy an application, IntApp, with minimal intrusion and no reboots. Part of the process was to restart the application after a successful installation. The crux was that the application has to run under the user credentials. The app starts every time a system is logged into, but with laptops, that could have been a long time until the user actually rebooted the machine. This could also have been done as a package in which the executable would have been executed, but I wanted verification that when it was executed, it was actually running. To achieve this, I wrote two powershell scripts. The first one is the detection method and the second is the one that actually executes the app.

The detection method first makes sure the executable is installed. If it is, it then looks for the process to see if it is running. If it's running, a success is returned to SCCM. If not, nothing is returned back to SCCM, therefor SCCM interprets that as a failure and proceeds to execute the installer. If the application is not installed, it also returns a failure to SCCM.

The installation will test to see if the executable is present. If not, it automatically fail the install with an exit code 1. If it is installed, it will then test for the process. If the process is present, it will return a success code back to SCCM. If it is not, it will then run the executable and retest to make sure it is present in memory.

Here is the detection method:


 $DesktopExtension = $Env:ProgramFiles + "\IntApp\Desktop Extension\IntappTimeDesktopExtension.exe"  
 If ((Test-Path -Path $DesktopExtension) -eq $true) {  
      $ProcessActive = Get-Process -Name IntappTimeDesktopExtension -ErrorAction SilentlyContinue  
      If ($ProcessActive.ProcessName -eq "IntappTimeDesktopExtension") {  
           Write-Host "Success"  
           exit 0  
      } else {  
           exit 0  
      }  
 } else {  
      exit 0  
 }  


Here is the application installation script:


 $DesktopExtension = $Env:ProgramFiles + "\IntApp\Desktop Extension\IntappTimeDesktopExtension.exe"  
 If ((Test-Path -Path $DesktopExtension) -eq $true) {  
      $ProcessActive = Get-Process -Name IntappTimeDesktopExtension -ErrorAction SilentlyContinue  
      If ($ProcessActive.ProcessName -ne "IntappTimeDesktopExtension") {  
           Start-Process -FilePath $env:ProgramFiles"\IntApp\Desktop Extension\IntappTimeDesktopExtension.exe" -ErrorAction SilentlyContinue  
           Start-Sleep -Seconds 2  
      } else {  
           Write-Host "Success"  
           exit 0  
      }  
      $ProcessActive = Get-Process -Name IntappTimeDesktopExtension -ErrorAction SilentlyContinue  
      If ($ProcessActive.ProcessName -eq "IntappTimeDesktopExtension") {  
           Write-Host "Success"  
           exit 0  
      } else {  
           exit 0  
      }  
 } else {  
      exit 1  
 }  
   

14 July 2015

PowerShell MSI Uninstaller By Application Name

Posted By: Mick Pletcher - 9:26 AM
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.




 Function Uninstall-MSIByName {  
      <#  
      .SYNOPSIS  
           Uninstall-MSIByName  
      .DESCRIPTION  
           Uninstalls an MSI application using the MSI file  
      .EXAMPLE  
           Uninstall-MSIByName -ApplicationName "Adobe Reader" -Switches "/qb- /norestart"  
      #>  
        
      Param ([String]  
           $ApplicationName,  
           [String]  
           $Switches)  
        
      #Declare Local Variables  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name Executable -Scope Local -Force  
      Set-Variable -Name Key -Scope Local -Force  
      Set-Variable -Name KeyName -Scope Local -Force  
      Set-Variable -Name Parameters -Scope Local -Force  
      Set-Variable -Name SearchName -Scope Local -Force  
      Set-Variable -Name TempKey -Scope Local -Force  
      Set-Variable -Name Uninstall -Scope Local -Force  
        
      $Uninstall = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $Uninstall += Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -Recurse -ea SilentlyContinue  
      $SearchName = "*" + $ApplicationName + "*"  
      $Executable = $Env:windir + "\system32\msiexec.exe"  
      Foreach ($Key in $Uninstall) {  
           $TempKey = $Key.Name -split "\\"  
           If ($TempKey[002] -eq "Microsoft") {  
                $Key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           } else {  
                $Key = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" + $Key.PSChildName  
           }  
           If ((Test-Path $Key) -eq $true) {  
                $KeyName = Get-ItemProperty -Path $Key  
                If ($KeyName.DisplayName -like $SearchName) {  
                     $TempKey = $KeyName.UninstallString -split " "  
                     If ($TempKey[0] -eq "MsiExec.exe") {  
                          Write-Host "Uninstall"$KeyName.DisplayName"....." -NoNewline  
                          $Parameters = "/x " + $KeyName.PSChildName + [char]32 + $Switches  
                          $ErrCode = (Start-Process -FilePath $Executable -ArgumentList $Parameters -Wait -Passthru).ExitCode  
                          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  
                          }  
                     }  
                }  
           }  
      }  
        
      #Cleanup Local Variables  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name Executable -Scope Local -Force  
      Remove-Variable -Name Key -Scope Local -Force  
      Remove-Variable -Name KeyName -Scope Local -Force  
      Remove-Variable -Name Parameters -Scope Local -Force  
      Remove-Variable -Name SearchName -Scope Local -Force  
      Remove-Variable -Name TempKey -Scope Local -Force  
      Remove-Variable -Name Uninstall -Scope Local -Force  
        
 }  
   

01 July 2015

Laptop Mandatory Reboot Management

Posted By: Mick Pletcher - 5:16 PM
Managing laptops in certain environments can be daunting. Reboots are a must every now and then, especially for monthly windows updates. With the sleep and hibernate features being enabled, the chances of a user rebooting a laptop become far less. A laptop can go weeks and even months without a reboot. Working in the legal industry, as I do, adds to the complexity of forcing reboots as you have the issue of not causing a reboot during a hearing or during a client meeting for instance. You want to be as unobtrusive as possible. You might say that this is not needed as SCCM could be setup to automatically perform this same function on a regular schedule. That is true. Where this becomes valuable is when you don't want to force a reboot on users that regularly reboot their machines. If they are already doing this, which we have a fair number of users that do, then there is no need to reboot an additional time that will inconvenience them.

To make this process as unobtrusive as possible, I have written the following two PowerShell scripts that work in conjunction with SCCM 2012 to give users the leisure of a full business day to reboot the machine. One script is the custom detection method and the other is the installer. 

The custom detection method works by reading the last event viewer 1074. It looks at the date of the ID and then sees if it is 14 days or older. This can be set to any number of days other than the 14 my firm has chosen. If it is 14 days old, the script then sets the registry key Rebooted to a DWORD value of 1 and fails the detection method. When the method fails, SCCM will then run the second PowerShell script. 

The second script operates by running the same detection method. Once it detects the same values, it resets the Rebooted Key to 0 and then returns the error code 3010 back to SCCM. SCCM then reruns the detection method. The detection method sees there has been 14 days or more and the Rebooted key is set to 0. It returns and error code 0 back to SCCM with a write-host of "Success". This tells SCCM the application ran successfully and to now process with the soft reboot, which was required by the 3010 return code. 

The final part is to configure the Computer Restart under the SCCM client settings. I configured ours to be 480 minutes, which is 8 hours, with a mandatory dialog window that cannot be closed the final 60 minutes. 

When the system reboots, the custom detection method runs again and sees there is a new 1074 entry in the event viewer, along with the registry key Rebooted being a 0, therefor it shows successfully installed. As the days progress and SCCM reruns the custom detection method, it will rerun the application script to reboot the machine again if the machine is not rebooted in the 14 allotted days. If the user reboots the machine every week, the SCCM application will never reboot the machine. 

Here are the links to the two scripts:

 <#  
 .SYNOPSIS  
   SCCM Reboot Detection Method  
 .DESCRIPTION  
   This script will read the last time a system rebooted from the event  
   viewer logs. It then calculates the number of days since that time. If  
   the number of days equals or exceeds the RebootThreshold variable, the  
   script will exit with a return code 0 and no data output. No data output   
   is read by SCCM as a failure. If the number of days is less than the  
   RebootThreshold, then a message is written saying the system is within  
   the threshold and the script exits with a return code of 0. SCCM reads  
   an error code 0 with data output as a success.   
 .Author  
   Mick Pletcher  
 .Date  
   30 June 2015  
 #>  
   
 $RebootThreshold = 14  
 $Today = Get-Date  
 $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
 $Architecture = $Architecture.OSArchitecture  
 $LastReboot = get-winevent -filterhashtable @{logname='system';ID=1074} -maxevents 1 -ErrorAction SilentlyContinue  
 if ($Architecture -eq "32-bit") {  
      if ((Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
           New-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
      }  
      $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
      $Rebooted = $Rebooted.Rebooted  
 } else {  
      if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
           New-Item "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
      }  
      $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
      $Rebooted = $Rebooted.Rebooted  
 }  
 if ($LastReboot -eq $null) {  
      $Difference = $RebootThreshold  
 } else {  
      $Difference = New-TimeSpan -Start $Today -End $LastReboot.TimeCreated  
      $Difference = [math]::Abs($Difference.Days)  
 }  
 #Write-Host "Reboot Threshold:"$RebootThreshold  
 #Write-Host "Difference:"$Difference  
 #Write-Host "Rebooted:"$Rebooted  
 if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 0)) {  
      Write-Host "Success"  
      exit 0  
 }  
 if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 1)) {  
      Write-Host "Success"  
      exit 0  
 }  
 if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 0)) {  
      exit 0  
 }  
 if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 1)) {  
      exit 0  
 }  
   



 <#  
 .SYNOPSIS  
   SCCM Mandatory Reboot  
 .DESCRIPTION  
   This script will read the last time a system rebooted from the event  
   viewer logs. It then calculates the number of days since that time. If  
   the number of days equals or exceeds the RebootThreshold variable, the  
   script will change the registry key Rebooted to a 0. It then exits with   
   an error code 3010, which tells SCCM 2012 to perform a soft reboot.  
 .Author  
   Mick Pletcher  
 .Date  
   30 June 2015  
 #>  
   
 #Declare Global Variables  
 Set-Variable -Name Architecture -Scope local -Force  
 Set-Variable -Name Difference -Scope Local -Force  
 Set-Variable -Name LastReboot -Scope Local -Force  
 Set-Variable -Name Rebooted -Scope Local -Force  
 Set-Variable -Name RebootThreshold -Scope Local -Force  
 Set-Variable -Name Today -Scope Local -Force  
   
 $Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
 $Architecture = $Architecture.OSArchitecture  
 if ($Architecture -eq "32-bit") {  
      if ((Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
           New-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
      }  
      $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
      $Rebooted = $Rebooted.Rebooted  
 } else {  
      if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $false) {  
           New-Item "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" | New-ItemProperty -Name Rebooted -Value 0 -Force | Out-Null  
      }  
      $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
      $Rebooted = $Rebooted.Rebooted  
 }  
 $RebootThreshold = 14  
 $Today = Get-Date  
 $LastReboot = get-winevent -filterhashtable @{logname='system';ID=1074} -maxevents 1 -ErrorAction SilentlyContinue  
 if ($LastReboot -eq $null) {  
      $Difference = $RebootThreshold 
 } else {  
      $Difference = New-TimeSpan -Start $Today -End $LastReboot.TimeCreated  
      $Difference = [math]::Abs($Difference.Days)  
 }  
 if (($Difference -ge $RebootThreshold) -and ($Rebooted -eq 0)) {  
      if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $true) {  
           Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" -Name Rebooted -Value 1 -Type DWORD -Force  
           $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
           $Rebooted = $Rebooted.Rebooted  
      } else {  
           Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" -Name Rebooted -Value 1 -Type DWORD -Force  
           $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
           $Rebooted = $Rebooted.Rebooted  
      }  
 }  
 if (($Difference -lt $RebootThreshold) -and ($Rebooted -eq 1)) {  
      if ((Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot") -eq $true) {  
           Set-ItemProperty -Name Rebooted -Value 0 -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot" -Type DWORD -Force  
           $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Reboot"  
           $Rebooted = $Rebooted.Rebooted  
      } else {  
           Set-ItemProperty -Name Rebooted -Value 0 -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot" -Type DWORD -Force  
           $Rebooted = Get-ItemProperty -Name Rebooted -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Reboot"  
           $Rebooted = $Rebooted.Rebooted  
      }  
      Write-Host "System is within"$RebootThreshold" Day Reboot Threshold"  
 }  
 Write-Host "Reboot Threshold:"$RebootThreshold  
 Write-Host "Difference:"$Difference  
 Write-Host "Rebooted:"$Rebooted  
   

16 June 2015

Deploying Workshare Professional

Posted By: Mick Pletcher - 10:00 AM





Deloying Workshare Professsional is by no means an easy task for configuring the installation if you try and edit the MSI with ORCA. When configuring the MSI to customize the installation of Workshare for office and document management integration, I found the easiest way is to set specific registry keys before the install takes place. If the keys are set first, the MSI will read those keys and configure the app during the installation. Once these keys are installed, you can then run the WorkshareProfessional.msi with the switches /qb- or /qn and nothing else. Of course I installed the prerequisites first before executing the Worshare msi.

32-Bit System:
[HKEY_LOCAL_MACHINE\SOFTWARE\Workshare\Install]
"ProfInterwovenModule"=dword:00000000
"ProfCompareModule"=dword:00000001
"ProfHummingbirdModule"=dword:00000000
"OfficeExcelIntegration"=dword:00000000
"OfficeOutlookIntegration"=dword:00000000
"OfficePowerPointIntegration"=dword:00000000
"OfficeWordIntegration"=dword:00000000
"ProfNetDocumentsModule"=dword:00000001
"ProfWorldoxModule"=dword:00000000
"ProfProtectModule"=dword:00000000
"ProfReviewModule"=dword:00000000
"ProfSecureFileTransferModule"=dword:00000000
"ProfSharepointModule"=dword:00000000

64-Bit System:
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Workshare\Install]
"ProfInterwovenModule"=dword:00000000
"ProfCompareModule"=dword:00000001
"ProfHummingbirdModule"=dword:00000000
"OfficeExcelIntegration"=dword:00000000
"OfficeOutlookIntegration"=dword:00000000
"OfficePowerPointIntegration"=dword:00000000
"OfficeWordIntegration"=dword:00000000
"ProfNetDocumentsModule"=dword:00000001
"ProfWorldoxModule"=dword:00000000
"ProfProtectModule"=dword:00000000
"ProfReviewModule"=dword:00000000
"ProfSecureFileTransferModule"=dword:00000000
"ProfSharepointModule"=dword:00000000


14 April 2015

Move PowerShell console during script execution

Posted By: Mick Pletcher - 10:32 PM














I heavily use PowerShell during the SCCM imaging process for installing each application. There are often times that I will watch the build to make sure everything is working correctly. When it calls a task sequence which calls a PowerShell script, the script will often open up in the top left of the screen and is covered up by the build status window. This function and the AutoIT script will allow for the console window to be moved while the script is executing. This does require executing the AutoIT script from the powershell script. There have been other methods shown for doing this, but from what I have seen, they are far more cumbersome.


AutoIT Script
 Sleep(500)  
 $WindowName = $CmdLine[1]  
 If WinExists($WindowName) Then  
      $aWinGetPos = WinGetPos($WindowName)  
      $Y = (@DesktopHeight / 2) - ($aWinGetPos[2] / 3)  
      $X = (@DesktopWidth / 2) - ($aWinGetPos[3] / 1)  
      WinMove($WindowName, "", $X, $Y)  
 EndIf  
   


PowerShell Function
 function MoveConsoleWindow {  
      $WindowName = $Host.ui.RawUI.WindowTitle  
      start-process -filepath "\\FileShare\MovePowerShellWindow\CenterPowerShellWindow.exe" -argumentlist $WindowName  
 }  
   

10 April 2015

Application List

Posted By: Mick Pletcher - 10:53 AM





This script will generate a list of installed applications minus those in the exclusion list text file. I created this script so the help desk could have a concise list of applications that need to be installed post-build. The script excludes all apps that are in the exclusion list, which includes everything in our build. I wrote the script so that it can be executed from SCCM on a per system basis. It could be modified to allow it to execute on remote machines.

The first thing to do is to create the ExclusionList.txt file and place it in the same directory as the script. Next is create a package in SCCM. That is all that is to it.

You can download the script from here.


 <#  
 .SYNOPSIS  
   Installed Applications  
 .DESCRIPTION  
   This will retrieve the list of installed applications from   
   add/remove programs  
 .Author  
   Mick Pletcher  
 .Date  
   09 April 2015  
 .EXAMPLE  
   powershell.exe -executionpolicy bypass -file InstalledApplications.ps1  
 #>  
   
 #Declare Global Variables  
 Set-Variable -Name Architecture -Scope Global -Force  
 Set-Variable -Name Applications -Scope Global -Force  
 Set-Variable -Name LogFile -Value "c:\Applications.csv" -Scope Global -Force  
 Set-Variable -Name RelativePath -Scope Global -Force  
   
 function Get-RelativePath {  
      $Global:RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent)+"\"  
 }  
   
 function Get-Architecture {  
      $Global:Architecture = Get-WmiObject -Class Win32_OperatingSystem | Select-Object OSArchitecture  
      $Global:Architecture = $Global:Architecture.OSArchitecture  
      #Returns 32-bit or 64-bit  
 }  
   
 function CreateLogFile {  
      #Define Local Variables  
      Set-Variable -Name Output -Scope Local -Force  
      Set-Variable -Name Temp -Scope Local -Force  
        
      if ((Test-Path $Global:LogFile) -eq $true) {  
           Remove-Item -Path $Global:LogFile -Force  
      }  
      if ((Test-Path $Global:LogFile) -eq $false) {  
           $Temp = New-Item -Path $Global:LogFile -ItemType file -Force  
      }  
      $Output = "Application Name"  
      Out-File -FilePath $Global:LogFile -InputObject $Output -Append -Force -Encoding UTF8  
                       
        
      #Cleanup Local Variables  
      Remove-Variable -Name Output -Scope Local -Force  
      Remove-Variable -Name Temp -Scope Local -Force  
 }  
   
 function GetAddRemovePrograms {  
      #Define Local Variables  
      Set-Variable -Name Applicationsx86 -Scope Local -Force  
      Set-Variable -Name Applicationsx64 -Scope Local -Force  
      Set-Variable -Name ARPx86 -Scope Local -Force  
      Set-Variable -Name ARPx64 -Scope Local -Force  
        
      $ARPx86 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"  
      $ARPx64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\"  
      if ($Global:Architecture -eq "32-bit") {  
           $Applicationsx86 = Get-ChildItem -Path $ARPx86 | ForEach-Object -Process {$_.GetValue("DisplayName")}  
      } else {  
           $Applicationsx86 = Get-ChildItem -Path $ARPx64 | ForEach-Object -Process {$_.GetValue("DisplayName")}  
           $Applicationsx64 = Get-ChildItem -Path $ARPx86 | ForEach-Object -Process {$_.GetValue("DisplayName")}  
      }  
      $Global:Applications = $Applicationsx86 + $Applicationsx64  
      $Global:Applications = $Global:Applications | Sort-Object  
      $Global:Applications = $Global:Applications | select -Unique  
        
      #Cleanup Local Memory  
      Remove-Variable -Name Applicationsx86 -Force  
      Remove-Variable -Name Applicationsx64 -Force  
      Remove-Variable -Name ARPx86 -Force  
      Remove-Variable -Name ARPx64 -Force  
 }  
   
 function GenerateReport {  
      #Define Local Variables  
      Set-Variable -Name Application -Scope Local -Force  
      Set-Variable -Name Exclusions -Scope Local -Force  
      Set-Variable -Name LogFile -Scope Local -Force  
      Set-Variable -Name Print -Value $true -Scope Local -Force  
        
      $Exclusions = Get-Content -Path $RelativePath"ExclusionList.txt"  
      foreach ($Application in $Global:Applications) {  
           if ($Application -ne $null) {  
                $Application = $Application.Trim()  
           }  
           if (($Application -ne "") -and ($Application -ne $null)) {  
                foreach ($Exclusion in $Exclusions) {  
                     $Exclusion = $Exclusion.Trim()  
                     if ($Application -like $Exclusion) {  
                          $Print = $false  
                     }  
                }  
                if ($Print -eq $true) {  
                     Write-Host $Application  
                     Out-File -FilePath $Global:LogFile -InputObject $Application -Append -Force -Encoding UTF8  
                }  
           }  
           $Print = $true  
      }  
   
      #Cleanup Local Variables  
      Remove-Variable -Name Application -Scope Local -Force  
      Remove-Variable -Name Exclusions -Scope Local -Force  
      Remove-Variable -Name LogFile -Scope Local -Force  
      Remove-Variable -Name Print -Scope Local -Force  
 }  
   
 cls  
 Get-RelativePath  
 Get-Architecture  
 CreateLogFile  
 GetAddRemovePrograms  
 GenerateReport  
   
 #Cleanup Global Variables  
 Remove-Variable -Name Architecture -Force  
 Remove-Variable -Name Applications -Force  
 Remove-Variable -Name LogFile -Force  
 Remove-Variable -Name RelativePath -Force  
   

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