Link to home
Start Free TrialLog in
Avatar of scsyeg
scsyeg

asked on

Startup Cleanup - Remove Registry values that that do not match a list

Hi Experts,

I am writing a script that removes unwanted startup entries.

I thought that I would start with the keys in
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run

If there is anywhere else you think I should look, please tell me (ie: the startup folder is next in line)

Anyways, I thought I would use powershell (I wrote a VBScript for this, but apparently, we don't want to use VBScript [if there is any argument for against vbscript vs powershell, please share]).

I think the best/easiest way is to have a x.FilterPhrases file (just a text file with line breaks and maybe headers that organize the types) and for powershell to remove all keys that do not have values in this file in the value name.

So far I have as follows:

$excluded = "string1"
Get-Item "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" |Where {$excluded -contains $_.Property} | Remove-Item -WhatIf

Open in new window


This returns:
What if: Performing operation "Remove Key" on Target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

This removes the entire Run, which I do not want and I cannot get Remove-ItemProperty to work properly.

Is there a better way of doing this? any warnings? (we backup the registry beforehand just incase). My previous approach was to export everything, filter through the export, keeping what I wanted, remove everything, restore the filtered export, but this lead to some keys not wanting to be imported and some errors.

An example of the filterPhrases file I have compiled so far:

:File Header - Do not Edit
Windows Registry Editor
HKEY_LOCAL_MACHINE
HKEY_CURRENT_USER

:AntiVirusCompaniesAndTerms
ESET
Symantec
McAfee
Trend Micro
TrendMicro
Sophos
Kaspersky
avast!
Webroot
BitDefender
F-Secure
AhnLab
Norton
Panda
VIPRE
AVG
Windows Defender
ZoneAlarm

:GeneralTerms
Security
Firewall
Fortinet
Spybot
BlackICE
fax
printer
GoogleDriveSync
StikyNot
Skype
Viber
VMWare

:Manufacturers
Lenovo
Dell
Hewlett Packard
ASUS
nVidia
AMD

:Misc
Classic Start Menu
Cisco

Open in new window

Avatar of footech
footech
Flag of United States of America image

I don't know what a filterphrases file is - maybe a VBScript concept?  You would have to specially parse the example file to do anything with it.
When working with the registry in PowerShell, an item is a key, and an item property is a value and its data.  So what you're trying to do is find (or exclude) properties where the name changes, and so you've got a moving target which makes it more difficult to hit.  There are also properties added to the returned objects by PowerShell which complicate it further.
Here's an example of deleting registry values (itemproperty) that don't have the name "string1".
$exclude = "string1","PSPath","PSParentPath","PSChildName","PSDrive","PSProvider"
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
$toDelete = Get-ItemProperty $key | Get-Member -MemberType NoteProperty | ? { $exclude -notcontains $_.Name } | % {$_.Name}
Remove-ItemProperty -Path $key -Name $toDelete -WhatIf

Open in new window

By the way, I don't typically advocate rewriting VBScript code to PowerShell.  If you've got something that works, stick with it.  If you want to rewrite it more as a learning exercise (or perhaps because of some mandate you have no control over), then all I can say is that it's generally poor practice to try to "translate" VBScript directly to PS.  You'll likely end up with a lot of convoluted and unnecessary code that way.  Instead it's best to learn "the PowerShell way", and then take those concepts and apply them to solving the problem the problem at hand.
it is probably easier to export run key to a .reg file, edit the .reg file and import it back by regedit (double-click on the .reg file). if you know the valid entries below run, you even could spare the export and edit step and simply import a file which you exported from another machine where the entries are fine.

Windows Registry Editor Version 5.00

[HKEY_USERS\S-1-5-21-251489984-1497240538-811669034-43870\Software\Microsoft\Windows\CurrentVersion\Run]
"OfficeSyncProcess"="\"C:\\Program Files (x86)\\Microsoft Office\\Office14\\MSOSYNC.EXE\""

Open in new window


Sara
Avatar of scsyeg
scsyeg

ASKER

@footech: I will test your code. filterPhrases is a made up extension for a text file that has the information I posted in it. I'm used to doing that in linux. A Get-Content should work I think? Do I always have to exclude: "PSPath","PSParentPath","PSChildName","PSDrive","PSProvider"?

The vb script I have at the moment does more along the lines of what sarabande suggests, that is it exports, edits export, deletes all, imports edited export, but that throws errors for some values when trying to import them back, so I want to only remove what is needed instead.

@sarabande thanks for the advice, my vbscript is similar (see the lines directly above). But that won't do exactly.
Avatar of scsyeg

ASKER

@footech: the code is throwing an error. says that a property with the name it found does not exist at the path. i tried adding \`" around it, same thing.

PS C:\Users\Armen\Desktop> C:\Users\Armen\Desktop\test.ps1
What if: Performing operation "Remove Property" on Target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run Property: Adobe ARM".
Remove-ItemProperty : Property Adobe ARM does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run.
At C:\Users\Armen\Desktop\test.ps1:4 char:20
+ Remove-ItemProperty <<<<  -Path $key -Name $toDelete -WhatIf
    + CategoryInfo          : InvalidArgument: (Adobe ARM:String) [Remove-ItemProperty], PSArgumentException
    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.RemoveItemPropertyCommand
 
What if: Performing operation "Remove Property" on Target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run Property: SunJavaUpdateSched".
Remove-ItemProperty : Property SunJavaUpdateSched does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run.
At C:\Users\Armen\Desktop\test.ps1:4 char:20
+ Remove-ItemProperty <<<<  -Path $key -Name $toDelete -WhatIf
    + CategoryInfo          : InvalidArgument: (SunJavaUpdateSched:String) [Remove-ItemProperty], PSArgumentException
    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.RemoveItemPropertyCommand
 
What if: Performing operation "Remove Property" on Target "Item: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run Property: VMware User Process".
Remove-ItemProperty : Property VMware User Process does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run.
At C:\Users\Armen\Desktop\test.ps1:4 char:20
+ Remove-ItemProperty <<<<  -Path $key -Name $toDelete -WhatIf
    + CategoryInfo          : InvalidArgument: (VMware User Process:String) [Remove-ItemProperty], PSArgumentException
    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.RemoveItemPropertyCommand

Open in new window


Seems like it's grabbing the correct values, but that Remove-ItemProperty is having issues.

Edit: these are not real errors, are they. the -WhatIf is the cause? It deletes when I take it out...Sorry I'm a newbie.
Get-Content should work for reading the file, but if you have blank lines or header/section lines you'll need to exclude those to avoid errors.  And yes, you'll always have to exclude "PSPath","PSParentPath","PSChildName","PSDrive","PSProvider" - those are properties that are added by PowerShell.  If you run the following you will see.
Get-ItemProperty $key | Get-Member

Open in new window

You could hard code those values into the script if you like and then just add the contents of the file.

I've seen a couple times where using -whatif with different cmdlets gives an error that doesn't reflect reality. Usually it's good, but when it's not I just fall back on doing the actual operation (no -whatif parameter) with some test value to make sure things work as expected.
Avatar of scsyeg

ASKER

Great; thanks. A couple final things:

How do I remove blank lines?
How do I use what's in the filterPhrases file as keywords, and not literals? ie: -notLike "*[filterPhrasesLine]*"

Tried some things and so far this is what I have; I am sure I could clean up the code a bit but I'm not sure how at the moment. Can you help?

$exclude = Get-Content .\Startup.FilterPhrases.txt
$exclude = $exclude -notlike "::*"
$exclude = $exclude |?{$_}
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
$toDelete = Get-ItemProperty $key | Get-Member -MemberType NoteProperty | ? { $exclude -notcontains $_.Name } | % {$_.Name}
foreach ($exclusionItem in $exclude)
{
  $toDelete = $toDelete -notlike $("*"+$exclusionItem+"*")
}
Remove-ItemProperty -Path $key -name $toDelete -WhatIf

Open in new window


Where Startup.FilterPhrases.txt:
::Default Properties - Do not Edit
PSPath
PSParentPath
PSChildName
PSDrive
PSProvider

::AntiVirusCompaniesAndTerms
ESET
Symantec
McAfee
Trend Micro
TrendMicro
Sophos
Kaspersky
avast!
Webroot
BitDefender
F-Secure
AhnLab
Norton
Panda
VIPRE
AVG
Windows Defender
ZoneAlarm

::GeneralTerms
Security
Firewall
Fortinet
Spybot
BlackICE
fax
printer
GoogleDriveSync
StikyNot
Skype
Viber
VMWare

::Manufacturers
Lenovo
Dell
Hewlett Packard
ASUS
nVidia
AMD

::Misc
Classic Start Menu
Cisco

Open in new window

I would replace lines 1-3 with:
$exclude = Get-Content .\Startup.FilterPhrases.txt | ? {$_ -notlike "::*" -and $_}

Open in new window


Not sure how you could each line so it isn't literal.  I've thought about it a bit and I'm not coming up with anything obvious.  Instead of doing the simple Where-Object filtering with -notcontains, you'd likely have to loop through every property and compare it to each element in $exclude individually.
Avatar of scsyeg

ASKER

Thanks again. The one question I have left is that line 37, the "? { $exclusionTerms -notcontains $_.Name }" doesnt really need to be there since I iterate through the list and filter without it, but breaks if I remove it.  Could you explain this?

Almost final product (set default parameters for demonstration):

#parameters passed in by user or set as default
Param(
  $filterFile = "Startup.FilterPhrases.txt",
  $filterPath = ".",
  $keyPath = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
)

#check if entered filterPath ends with "\". If not, add "\".
if (!($filterPath.EndsWith("\"))){
  $filterPath = $filterPath + "\"
}

#check that the keyPath is correctly formatted as a powershell path
$hives = @{
  "^HKEY_CLASSES_ROOT\\" = "HKCR:\"; 
  "^HKEY_CURRENT_USER\\" = "HKCU:\"; 
  "^HKEY_LOCAL_MACHINE\\" = "HKLM:\"; 
  "^HKEY_USERS\\" = "HKU:\"; 
  "^HKEY_CURRENT_CONFIG\\" = "HKCC:\";
  "^HKCR\\" = "HKCR:\"; 
  "^HKCU\\" = "HKCU:\"; 
  "^HKLM\\" = "HKLM:\"; 
  "^HKU\\" = "HKU:\"; 
  "^HKCC\\" = "HKCC:\"
}

foreach ($hive in $hives.keys){
  if ($keyPath -match $hive) {
    $keyPath = $keyPath -replace $hive, $hives.$hive
  }
}

#open file, store as array and filter out comment lines that start with "::" and blank lines
$exclusionTerms = Get-Content $filterPath$filterFile | ? {$_ -notlike "::*" -and $_}

#return properties from path
$propertiesToDelete = Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | ? { $exclusionTerms -notcontains $_.Name } | % {$_.Name}

#filter properties from path excluding any that contain terms in exclusionTerms
foreach ($term in $exclusionTerms)
{
  $propertiesToDelete = $propertiesToDelete -notlike $("*"+$term+"*")
}

#$propertiesToDelete returns True if it does not have anything left after the exclusion process. 
#If nothing, return that it does not have anything to remove. Otherwise, remove and return properties removed
If ($propertiesToDelete -eq $True){
  $output = 'No keys to delete from "' + $keyPath + '" with exclusion terms in ' + $filterPath + $filterFile
}
Else{
  $output = "Removed: " + ($propertiesToDelete -join ", ") + " from " + $keyPath
  Remove-ItemProperty -Path $keyPath -name $propertiesToDelete
}
Write-Host $output

Open in new window

Here's something I came up with for treating each line as a keyword.
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
$keyProperties = Get-ItemProperty $key | Get-Member -MemberType NoteProperty | % {$_.Name}
$perKeyExclude = $keyProperties | % `
{
    $prop = $_
    $exclude | % {
        If ( $prop -match $_ )
        { $prop }
    }
}
$toDelete = $keyProperties | ? { $perKeyExclude -notcontains $_ }
Remove-ItemProperty -Path $key -Name $toDelete -WhatIf -ErrorAction SilentlyContinue

Open in new window


I don't think it's any better than what you have already, just a slightly different approach.  Yours gets an initial list of properties to delete, and then trims down that list.  Mine gets an initial list of all properties, and then adds on to the exclusion list, then trims down the properties to delete using that built-up exclusion list.

I don't see anything that would cause your script to break by changing line 17.  In my testing it appears to function fine.

BTW, on line 22, instead of $("*"+$term+"*") you could use "*$term*"
It's probably more a preference, but I tend to avoid concatentation when I can as it usually involves more typing.
Avatar of scsyeg

ASKER

Thank you for your input. To my un-powershell-trained eyes, my code is easier to follow, so I am tempted to leave it as is. Same with the "*$term*" since my way leaves variables red and strings brown vs your way makes it all brown and may be mistaken for a regex (?).

This works:
$propertiesToDelete = Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | ? { $exclusionTerms -notcontains $_.Name } | % {$_.Name}

Open in new window


This throws an error:
$propertiesToDelete = Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | % {$_.Name}

Remove-ItemProperty : Cannot bind argument to parameter 'Name' because it is an empty array.
At C:\Users\Armen\Desktop\removeRegistryProperties.ps1:52 char:43
+   Remove-ItemProperty -Path $keyPath -name <<<<  $propertiesToDelete
    + CategoryInfo          : InvalidData: (:) [Remove-ItemProperty], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyArrayNotAllowed,Microsoft.PowerShell.Commands.RemoveItemPropertyCommand

Open in new window

I'm using PS 3.0 ISE, and the variable is still red, while the rest of the string is brown.
No big deal, as I said it's probably more a preference.

Check the contents of $propertiesToDelete.  There's probably an empty element.
$propertiesToDelete | % {"'$_'"}

Open in new window

When I run your code, changing line 37, and using -whatif for Remove-ItemProperty, I get no error.  But if I manually add an element with an empty string or $null, then running the Remove-ItemProperty produces the error you posted.
Avatar of scsyeg

ASKER

From an efficiency standpoint though, concatenation is an extra, seemingly unnecessary process. So technically, I think you're way is more correct.

Got it. It's odd though. Sometimes it seems like when it's an empty array, it returns "True" and sometimes nothing.

What's the logic behind that?
Are you sure it's an array?
$propertiesToDelete.GetType()

It's probably from the -notlike comparison.
When the input to an operator is a scalar value, comparison operators
return a Boolean value. When the input is a collection of values, the
comparison operators return any matching values. If there are no matches
in a collection, comparison operators do not return anything.
If $propertiesToDelete is a string, then the -notlike comparion could end up being a boolean.
Try this mod to line 37 which will force it to always be an array.
$propertiesToDelete = @(Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | % {$_.Name})

Open in new window

Avatar of scsyeg

ASKER

This is the whole code as it stands. In response to forcing it to be an array, do you forsee at all if what I have changed it to (line 37 and 47) will have issues?

I have added some flexibility to entered values and polished it up a bit. If you think this is complete, I will go ahead and mark your next comment as the answer referring to this one for all of your wonderful help. Thanks very much.

#parameters passed in by user or set as default
Param(
  $filterFile,
  $filterPath = ".",
  $keyPath
)

#check if entered filterPath ends with "\". If not, add "\".
if (!($filterPath.EndsWith("\"))){
  $filterPath = $filterPath + "\"
}

#check that the keyPath is correctly formatted as a powershell path
$hives = @{
  "^HKEY_CLASSES_ROOT\\" = "HKCR:\"; 
  "^HKEY_CURRENT_USER\\" = "HKCU:\"; 
  "^HKEY_LOCAL_MACHINE\\" = "HKLM:\"; 
  "^HKEY_USERS\\" = "HKU:\"; 
  "^HKEY_CURRENT_CONFIG\\" = "HKCC:\";
  "^HKCR\\" = "HKCR:\"; 
  "^HKCU\\" = "HKCU:\"; 
  "^HKLM\\" = "HKLM:\"; 
  "^HKU\\" = "HKU:\"; 
  "^HKCC\\" = "HKCC:\"
}

foreach ($hive in $hives.keys){
  if ($keyPath -match $hive) {
    $keyPath = $keyPath -replace $hive, $hives.$hive
  }
}

#open file, store as array and filter out comment lines that start with "::" and blank lines
$exclusionTerms = Get-Content $filterPath$filterFile | ? {$_ -notlike "::*" -and $_}

#return properties from path
$propertiesToDelete = Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | % {$_.Name}

#filter properties from path excluding any that contain terms in exclusionTerms
foreach ($term in $exclusionTerms)
{
  $propertiesToDelete = $propertiesToDelete -notlike $("*"+$term+"*")
}

#$propertiesToDelete returns True if it does not have anything left after the exclusion process. 
#If nothing, return that it does not have anything to remove. Otherwise, remove and return properties removed
If ($propertiesToDelete.count -eq 0){
  $output = 'No keys to delete from "' + $keyPath + '" with exclusion terms in ' + $filterPath + $filterFile
}
Else{
  $output = "Removed: " + ($propertiesToDelete -join ", ") + " from " + $keyPath
  Remove-ItemProperty -Path $keyPath -name $propertiesToDelete
}
Write-Host $output

Open in new window

Line 47 should work if you make the change to line 37.  What you just posted doesn't show the change I suggested above.
Avatar of scsyeg

ASKER

I know. It seems to work without it. It only returned True when the "? { $exclusionTerms -notcontains $_.Name }" was part of line 37.

That's what was breaking line 47 when I had it check for "True". Now that it's not there, checking the count works.

I will include the change you mentioned if you think it's necessary.
I think it would break if the query only returned one key, so yes I would say the change is necessary.  It's possible the systems you query always have more than one key in the registry location, in which case you wouldn't see an error, but why take the chance?
Avatar of scsyeg

ASKER

Sounds good. The one thing that I'm considering adding is a check that the file itself is not blank. As it stands, if it's completely blank, it does nothing - not even throws the error for the "PSPath, PSParentPath, PSChildName, PSDrive, PSProvider" it only says that there was nothing to remove based on the file input - and does not remove anything - which is a good thing, but unintended.

Here is the final code.

#parameters passed in by user or set as default
Param(
  $filterFile,
  $filterPath = ".",
  $keyPath
)

#check if entered filterPath ends with "\". If not, add "\".
if (!($filterPath.EndsWith("\"))){
  $filterPath = $filterPath + "\"
}

#check that the keyPath is correctly formatted as a powershell path
$hives = @{
  "^HKEY_CLASSES_ROOT\\" = "HKCR:\"; 
  "^HKEY_CURRENT_USER\\" = "HKCU:\"; 
  "^HKEY_LOCAL_MACHINE\\" = "HKLM:\"; 
  "^HKEY_USERS\\" = "HKU:\"; 
  "^HKEY_CURRENT_CONFIG\\" = "HKCC:\";
  "^HKCR\\" = "HKCR:\"; 
  "^HKCU\\" = "HKCU:\"; 
  "^HKLM\\" = "HKLM:\"; 
  "^HKU\\" = "HKU:\"; 
  "^HKCC\\" = "HKCC:\"
}

foreach ($hive in $hives.keys){
  if ($keyPath -match $hive) {
    $keyPath = $keyPath -replace $hive, $hives.$hive
  }
}

#open file, store as array and filter out comment lines that start with "::" and blank lines
$exclusionTerms = Get-Content $filterPath$filterFile | ? {$_ -notlike "::*" -and $_}

#return properties from path
$propertiesToDelete = @(Get-ItemProperty $keyPath | Get-Member -MemberType NoteProperty | % {$_.Name})

#filter properties from path excluding any that contain terms in exclusionTerms
foreach ($term in $exclusionTerms)
{
  $propertiesToDelete = $propertiesToDelete -notlike $("*"+$term+"*")
}

#$propertiesToDelete returns True if it does not have anything left after the exclusion process. 
#If nothing, return that it does not have anything to remove. Otherwise, remove and return properties removed
If ($propertiesToDelete.count -eq 0){
  $output = 'No keys to delete from "' + $keyPath + '" with exclusion terms in ' + $filterPath + $filterFile
}
Else{
  $output = "Removed: " + ($propertiesToDelete -join ", ") + " from " + $keyPath
  Remove-ItemProperty -Path $keyPath -name $propertiesToDelete
}
Write-Host $output

Open in new window

Substitute the following for line 34.
try {
    $exclusionTerms = Get-Content $filterPath$filterFile -errorAction Stop | ? {$_ -notlike "::*" -and $_}
} catch {
    Write-Host "Input file not found.`nExiting..." -foregroundColor Red
}

Open in new window

Avatar of scsyeg

ASKER

I will look at the previous post ASAP. More significantly, if there is no keys in the location (HKLM:\...) it throws an error. How can I deal with this smoothly? Another Try/Catch?
Avatar of scsyeg

ASKER

most importantly, I just realized that I need to remove the entries from all HKCU's. Anyway to do that? adding the HKCU path only removes it from the current user by definition. I need to be able to filter all HKCU:\...\Run items
ASKER CERTIFIED SOLUTION
Avatar of footech
footech
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Using HKU to go thru all profiles requires to mount each user's hive temporarily in registry. There is no in-built way to do that, so all I could find was to use reg load, but that doesn't work well and has a lot of issues in PowerShell.
So best way IMHO is to use a logon script, which checks if it has been run for that user already (e.g. by maintaining a file with users).

Regarding the exclusion list, I would build a regex from the lines (resulting e.g. in 'keyword1|keyword2'), and apply the -nomatch operator instead of looping thru the exclusion list.
Avatar of scsyeg

ASKER

The answer to my question is spread across several posts. This user was amazing at helping.
http:#a40460758 was not helpful at all? Though I discussed how you could implement applying to all users?