PowerShell: How to find and uninstall a MS Office Update - powershell

I've been hunting for a clean way to uninstall an MSOffice security update on a large number of workstations. I've found some awkward solutions, but nothing as clean or general like using PowerShell and get-wmiobject with Win32_QuickFixEngineering and the .Uninstall method on the resulting object.
[Apparently, Win32_QuickFixEngineering only refers to Windows patches. See: http://social.technet.microsoft.com/Forums/en/winserverpowershell/thread/93cc0731-5a99-4698-b1d4-8476b3140aa3 ]
Question 1: Is there no way to use get-wmiobject to find MSOffice updates? There are so many classes and namespaces, I have to wonder.
This particualar Office update (KB978382) can be found in the registry here (for Office Ultimate):
HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\{91120000-002E-0000-0000-0000000FF1CE}_ULTIMATER_{6DE3DABF-0203-426B-B330-7287D1003E86}
which kindly shows the uninstall command of:
msiexec /package {91120000-002E-0000-0000-0000000FF1CE} /uninstall {6DE3DABF-0203-426B-B330-7287D1003E86}
and the last GUID seems constant between different versions of Office.
I've also found the update like this:
$wu = new-object -com "Microsoft.Update.Searcher"
$wu.QueryHistory(0,$wu.GetTotalHistoryCount()) | where {$_.Title -match "KB978382"}
I like this search because it doesn't require any poking around in the registry, but:
Question 2: If I've found it like this, what can I do with the found information to facilitate the Uninstall?
Thanks

Expanding on your second approach (using Microsoft.Update.Searcher), this solution should allow you to search for an update by Title then uninstall it:
$TitlePattern = 'KB978382'
$Session = New-Object -ComObject Microsoft.Update.Session
$Collection = New-Object -ComObject Microsoft.Update.UpdateColl
$Installer = $Session.CreateUpdateInstaller()
$Searcher = $Session.CreateUpdateSearcher()
$Searcher.QueryHistory(0, $Searcher.GetTotalHistoryCount()) |
Where-Object { $_.Title -match $TitlePattern } |
ForEach-Object {
Write-Verbose "Found update history entry $($_.Title)"
$SearchResult = $Searcher.Search("UpdateID='$($_.UpdateIdentity.UpdateID)' and RevisionNumber=$($_.UpdateIdentity.RevisionNumber)")
Write-Verbose "Found $($SearchResult.Updates.Count) update entries"
if ($SearchResult.Updates.Count -gt 0) {
$Installer.Updates = $SearchResult.Updates
$Installer.Uninstall()
$Installer | Select-Object -Property ResultCode, RebootRequired, Exception
# result codes: http://technet.microsoft.com/en-us/library/cc720442(WS.10).aspx
}
}

Related

If else statement inside foreach results in overwrite each time it loops

I apologize for the unclear title. I'm finding it hard to articulate my problem. I'm writing a script in powershell for the first time. I primarily use python for short scripts but for this task I'm forced to use powershell because of some limitations where I need to use powercli cmdlets. Let me quickly explain the problem. This is to create and/or assign tags to vms in vsphere.
I read the list of VMs into a variable $vms2tag. These are the ones that need to be tagged.
I read a json file into a variable and create tag name and description variables based on the data in the json (there's key value pairs that i can directly plug into the names and descriptions) This file also has a 'Server' key which has a value of the VM name exactly as it would appear in "Output-VM.csv" file. This file has data about every single VM that exists. Only ones that need tagged the ones in $vms2tag
Based on some if else conditions like if tag category exists, or if tag exists, it will either create one or use/assign one.
Basically the following code "works" in the sense it will create these tags BUT, it will quickly get overwritten by the next $vm until it keeps overwriting each time and the only tag that sticks around on all the $vms is the one created for the last VM in the list.
$myJson = Get-Content 'C:\For-Powershell.json'| Out-String | ConvertFrom-Json
$vms2tag = Get-Content 'C:\Output-VM.txt'
foreach ($vm in $vms2tag) {
For ($j=0; $j -lt $myJson.Length; $j++) {
if ($vm -eq $myJson.Server[$j]) {
Write-Output "Match!"
# Variables for Application Owner
$nameAO = [string]$myJson.Application_Owner[$j]
$descriptionAO = [string]$myJson.Application_Owner[$j]
# check if Tag Category and/or Tag exist
if ((Get-TagCategory -Name "app_owner") -eq $null) {
New-TagCategory -Name "app_owner" -Cardinality "Multiple"
}
if ((Get-Tag -Category "app_owner" | Set-Tag -Name $nameAO -Description $descriptionAO) -eq $null) {
$myTagAO = Get-TagCategory -Name "app_owner" | New-Tag -Name $nameAO -Description $descriptionAO
New-TagAssignment -Tag $myTagAO -Entity $myJson.Server[$j]
}
else {
$myTagAO = Get-Tag -Category "app_owner" | Set-Tag -Name $nameAO -Description $descriptionAO
New-TagAssignment -Tag $myTagAO -Entity $myJson.Server[$j]
}
}
}
}
I tested while the script runs and the tag is properly applied to the VM based on its data but when I refresh it after the script completes, all the tags on each VM exist but they are incorrect as they contain the information that's valid only for the last VM in the $vms2tag list. It seems pretty simple but I just don't see where I'm messing up. My guess is something with if else statements is nested incorrectly? It took me a while (~6 hours) to get this to work as I had other issues with the script but when I finally got the tags to correctly set based on the other conditions, I ended up with this problem so it's possible I'm just burnt out and not seeing it.
The problem is with the Tag logic. The following line is overwriting existing tags every loop:
if ((Get-Tag -Category "app_owner" | Set-Tag -Name $nameAO -Description $descriptionAO) -eq $null) {
The Set-Tag cmdlet should never be used in a test to find existing tags.
I would write the test and assignment block like the following:
$myTagAO = Get-Tag -Category "app_owner" -Name $nameAO -ErrorAction SilentlyContinue
if ($myTagAO -eq $null) {
$myTagAO = Get-TagCategory -Name "app_owner" | New-Tag -Name $nameAO -Description $descriptionAO
}
New-TagAssignment -Tag $myTagAO -Entity $myJson.Server[$j]
This ensures that each tag is only created once, with the appropriate description.

Missing AD module and can't get it, need something similar or something to simulate it

So I'm trying to output a complete KB list for all computers on a server (which works on one computer) but it doesn't recognize Get-ADcomputer as a cmdlet. When checking various sources, it appears that the AD module isn't included. As I'm doing this on a work computer/server I'm hesitant to download anything or anything of that nature.
Is there any way I can achieve the following without using the AD module or someway I might be missing how to import the module (if it exists, which I don't think it does on this system)?
# 1. Define credentials
$cred = Get-Credential
# 2. Define a scriptblock
$sb = {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
if ($_.Title -match "\(KB\d{6,7}\)") {
# Split returns an array of strings
$Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
} else {
$Title = $_.Title
}
$Result = $null
switch ($_.ResultCode) {
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
New-Object -TypeName PSObject -Property #{
InstalledOn = Get-Date -Date $_.Date;
Title = $Title;
Name = $_.Title;
Status = $Result
}
} | Sort-Object -Descending:$false -Property InstalledOn | Where {
$_.Title -notmatch "^Definition\sUpdate"
}
}
#Get all servers in your AD (if less than 10000)
Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree -Filter {
(OperatingSystem -like "Windows*Server*")
} | ForEach-Object {
# Get the computername from the AD object
$computer = $_.Name
# Create a hash table for splatting
$HT = #{
ComputerName = $computer ;
ScriptBlock = $sb ;
Credential = $cred;
ErrorAction = "Stop";
}
# Execute the code on remote computers
try {
Invoke-Command #HT
} catch {
Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
}
} | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize
You've got 3 options:
First is to just install the RSAT feature for AD which will include the AD module. This is probably the best option unless there is something specific preventing it. If you're running your script from a client operating systems you need to install the RSAT first, though.
Option 2 (which should only be used if adding the Windows feature is somehow an issue) is to download and use the Quest AD tools, which give very similar functionality, but it looks like Dell is doing their best to hide these now so that may be difficult to locate...
Option 3 is to use the .NET ADSI classes to access AD directly, which will work without any additional downloads on any system capable of running PowerShell. If you'd like to go this route you should check out the documentation for the interface Here and for the System.DirectoryServices namespace Here.
Edit
Just noticed the last part of your question, what do you mean by "a complete KB list"? Not just Windows updates or things updated manually or whatever? What else would be in a list of Windows updates that was not a Windows update?
You have not mentioned the OSes you are using but in general if you have a server 2008 R2 or above, all you have to do it activate the RSAT feature AD PowerShell Module and you will have the cmdlet you are looking for.
On a client machine, you 'have to' install RSAT, and then activate the features. You can take a look at the technet article for more info: https://technet.microsoft.com/en-us/library/ee449483(v=ws.10).aspx
If you don't want to use that option, then you will have to use .NET ADSI classes. There are tons of examples on how to do this, it basically boils down to a couple of lines really. Technet has examples on this as well: https://technet.microsoft.com/en-us/library/ff730967.aspx

PowerShell: Get the MSI product code out of a MSI file without installing?

Is there a possibility to retreive the MSI product code out of a MSI file without installing it with PowerShell? I want to compare the product code of a MSI file with the MSI codes installed on a maschine to find out if the file was installed in the past.
Here is a script that reads the product code based on [this article][1]:
$path = "pathto.msi"
$comObjWI = New-Object -ComObject WindowsInstaller.Installer
$MSIDatabase = $comObjWI.GetType().InvokeMember("OpenDatabase","InvokeMethod",$Null,$comObjWI,#($Path,0))
$Query = "SELECT Value FROM Property WHERE Property = 'ProductCode'"
$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)
$Value now contains the product code.
[1]: http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/
A shorter way to get the ProductCode from a MSI package:
Get-AppLockerFileInformation -Path "C:\PathTo\my.msi" | select -ExpandProperty Publisher | Select BinaryName

how to use PowerShell to inventory Scheduled Tasks

Does anyone have a link or script that uses PowerShell to inventory the Scheduled Tasks on a server, including the Action?
I am able to get the Scheduled Service com object and what I would call "top level" properties (name, state, lastruntime), but would like to also get information from the "Actions" part of the Schedule Tasks (essentially, the name of Scheduled Task and its commandline).
For example:
$schedule = new-object -com("Schedule.Service")
$schedule.connect()
$tasks = $schedule.getfolder("\").gettasks(0)
$tasks | select Name, LastRunTime
foreach ($t in $tasks)
{
foreach ($a in $t.Actions)
{
$a.Path
}
}
The above snippet of code works in terms of listing the tasks; but the loop on the Actions simply does not seem to do anything, no error, no output whatsoever.
Any help would be appreciated.
This is probably very similar to current answers, but I wrote a quick script to get you going. The problem with your current script is that there is no Actions property in a task. You need to extract it from the xml task-definition that the comobject provides. The following script will return an array of objects, one per scheduled task. It includes the action if the action is to run one or more command. It's just to get you going, so you need to modify it to include more of the properties if you need them.
function getTasks($path) {
$out = #()
# Get root tasks
$schedule.GetFolder($path).GetTasks(0) | % {
$xml = [xml]$_.xml
$out += New-Object psobject -Property #{
"Name" = $_.Name
"Path" = $_.Path
"LastRunTime" = $_.LastRunTime
"NextRunTime" = $_.NextRunTime
"Actions" = ($xml.Task.Actions.Exec | % { "$($_.Command) $($_.Arguments)" }) -join "`n"
}
}
# Get tasks from subfolders
$schedule.GetFolder($path).GetFolders(0) | % {
$out += getTasks($_.Path)
}
#Output
$out
}
$tasks = #()
$schedule = New-Object -ComObject "Schedule.Service"
$schedule.Connect()
# Start inventory
$tasks += getTasks("\")
# Close com
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) | Out-Null
Remove-Variable schedule
# Output all tasks
$tasks
Ex. of output
PS > .\Untitled1.ps1 | ? { $_.Name -eq "test" }
Actions : notepad.exe c:\test.txt
calc.exe
Path : \test
Name : test
LastRunTime : 30.12.1899 00:00:00
NextRunTime : 17.03.2013 13:36:38
Get the PowerShellPack from the W7 RK, and try get-scheduledtask
http://archive.msdn.microsoft.com/PowerShellPack
Excerpt From MSDN:
The Windows 7 Resource Kit PowerShell Pack contains 10 modules to do all sorts of interesting things with PowerShell. Import-Module PowerShellPack actually imports 10 modules for you to use. Here’s a brief overview of each of the modules.
WPK Create rich user interfaces quick and easily from Windows PowerShell. Think HTA, but easy. Over 600 scripts to help you build quick user interfaces
TaskScheduler List scheduled tasks, create or delete tasks
FileSystem Monitor files and folders, check for duplicate files, and check disk space
IsePack Supercharge your scripting in the Integrated Scripting Environment with over 35 shortcuts
DotNet Explore loaded types, find commands that can work with a type, and explore how you can use PowerShell, DotNet and COM together
PSImageTools Convert, rotate, scale, and crop images and get image metadata
PSRSS Harness the FeedStore from PowerShell
PSSystemTools Get Operating System or Hardware Information
PSUserTools Get the users on a system, check for elevation, and start-processaadministrator
PSCodeGen Generates PowerShell scripts, C# code, and P/Invoke
Another way would be a script I wrote called Get-ScheduledTask.ps1, available in this article:
How-To: Use PowerShell to Report on Scheduled Tasks
In this way you only need this single script and you don't need to download or install anything else.
Bill
I know I'm late to the party, but the answer provided by #Frode F., while it works, is technically not correct.
You can access items of the Actions collection of a scheduled task via PowerShell, it's just not immediately obvious. I had to figure this out myself today as well.
Here's the code to do this all in PowerShell, without having to muck around with XML:
# I'm assuming that you have a scheduled task object in the variable $task:
$taskAction = $task.Definition.Actions.Item.Invoke(1) # Collections are 1-based
That's all there is to getting a single item out of the collection without using foreach.
Because the Actions property is a collection which contains a parameterized property Item (e.g. in C# you would write myTask.Actions[0] or in VB myTask.Actions.Item(1)), PowerShell represents the Item property as a PSParameterizedProperty object. To call the methods associated with the property, you use the Invoke method (for the getter) and InvokeSet method (for the setter).
I ran a quick test running the OP's code and it worked for me (I'm running PowerShell 4.0, however, so maybe that has something to do with it):
$schedule = new-object -com("Schedule.Service")
$schedule.connect()
$tasks = $schedule.getfolder("\").gettasks(0)
$tasks | select Name, LastRunTime
foreach ($t in $tasks)
{
foreach ($a in $t.Actions)
{
Write-Host "Task Action Path: $($a.Path)" # This worked
Write-Host "Task Action Working Dir: $($a.workingDirectory)" # This also worked
}
$firstAction = $t.Actions.Item.Invoke(1)
Write-Host "1st Action Path: $($firstAction.Path)"
Write-Host "1st Action Working Dir: $($firstAction.WorkingDirectory)"
}
HTH.
here a quick one based on: https://blogs.technet.microsoft.com/heyscriptingguy/2015/01/17/weekend-scripter-use-powershell-to-document-scheduled-tasks/
Uses Powershell: Get-ScheduledTask and Get-ScheduledTaskInfo
### run like >> Invoke-Command -ComputerName localhost, server1, server2 -FilePath C:\tmp\Get_WinTasks.ps1
$taskPath = "\"
$outcsv = "c:\$env:COMPUTERNAME-WinSchTaskDef.csv"
Get-ScheduledTask -TaskPath $taskPath |
ForEach-Object { [pscustomobject]#{
Server = $env:COMPUTERNAME
Name = $_.TaskName
Path = $_.TaskPath
Description = $_.Description
Author = $_.Author
RunAsUser = $_.Principal.userid
LastRunTime = $(($_ | Get-ScheduledTaskInfo).LastRunTime)
LastResult = $(($_ | Get-ScheduledTaskInfo).LastTaskResult)
NextRun = $(($_ | Get-ScheduledTaskInfo).NextRunTime)
Status = $_.State
Command = $_.Actions.execute
Arguments = $_.Actions.Arguments }} |
Export-Csv -Path $outcsv -NoTypeInformation

How to pin to start menu using PowerShell

I can pin some programs to taskbar on Win7 using PowerShell.
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Tas&kbar'}
if ($verb) {$verb.DoIt()}
How do I modify the above code to pin a program to the Start menu?
Another way
$sa = new-object -c shell.application
$pn = $sa.namespace($env:windir).parsename('notepad.exe')
$pn.invokeverb('startpin')
Or unpin
$pn.invokeverb('startunpin')
Use the code below
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Start Men&u'}
if ($verb) {$verb.DoIt()}
Note: the change is in the fourth line.
The main problem with most of the solution is that they enumerate the verbs on a file, search for the string to perform the action (“Pin to Startmenu” etc.) and then execute it. This does not work if you need to support 30+ languages in your company, except you use external function to search for the localized command (see answer from shtako-verflow).
The answer from Steven Penny is the first that is language neutral and does not need any external code. It uses the verbs stored in the registry HKEY_CLASSES_ROOT\CLSID\{90AA3A4E-1CBA-4233-B8BB-535773D48449} and HKEY_CLASSES_ROOT\CLSID\{a2a9545d-a0c2-42b4-9708-a0b2badd77c8}
Based on this, here’s the code we are now using:
function PinToTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarpin"
}
function UnpinFromTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarunpin"
}
function PinToStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startpin"
}
function UnpinFromStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startunpin"
}
function ExecuteVerb {
param(
[Parameter(Mandatory=$true)][string]$File,
[Parameter(Mandatory=$true)][string]$Verb
)
$path = [System.Environment]::ExpandEnvironmentVariables($File)
$basePath = split-path $path -parent #retrieve only the path File=C:\Windows\notepad.exe -> C:\Windows
$targetFile = split-path $path -leaf #retrieve only the file File=C:\Windows\notepad.exe -> notepad.exe
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace($basePath)
if ($folder)
{
$item = $folder.Parsename($targetFile)
if ($item)
{
$item.invokeverb($Verb)
# "This method does not return a value." (http://msdn.microsoft.com/en-us/library/windows/desktop/bb787816%28v=vs.85%29.aspx)
# Therefore we have no chance to know if this was successful...
write-host "Method [$Verb] executed for [$path]"
}
else
{
write-host "Target file [$targetFile] not found, aborting"
}
}
else
{
write-host "Folder [$basePath] not found, aborting"
}
}
#PinToTaskbar "%WINDIR%\notepad.exe"
#UnpinFromTaskbar "%WINDIR%\notepad.exe"
PinToStartmenu "%WINDIR%\notepad.exe"
#UnpinFromStartmenu "%WINDIR%\notepad.exe"
See the script (international) here : http://gallery.technet.microsoft.com/scriptcenter/b66434f1-4b3f-4a94-8dc3-e406eb30b750
If you want to add an action like Pin to Modern UI interface (Windows 8), at $verbs, add 51201
Steven Penny's second answer above worked well for me. Here are a couple more tidbits.
It's doing COM through PowerShell, so you can do the same thing with pretty much any COM client. For example, here's an AutoHotkey version.
Shell := ComObjCreate("Shell.Application")
Target := Shell.Namespace(EnvGet("WinDir")).ParseName("Notepad.exe")
Target.InvokeVerb("startpin")
VBScript or InnoSetup would look almost the same except for the function used to create the object.
I also found that I have one program that pinned OK, but didn't have the right icon and/or description because of limitations in the compiler. I just made a little 1-line WinForms app that starts the target with Process.Start, and then added the appropriate icon, and the name I wanted in the Start Menu in the Title property in AppInfo.cs.