Get executable path name for executables of running services

I want to get the path of the executable (e.g. C:\Windows\System\example.exe) from all the running services. I do NOT wanna have any additional config parameter in there. Those paths I wish to add to a variable $services. A foreach loop should finally call Get-ACL to check the permissions of the service executable.

So what I tried is this:

$services=Get-WmiObject win32_service | 
select @{Expression={(($_.PathName.replace('"',''))) -replace'(^.*\.exe).*','"$1"'}} | Out-String | Format-List

I admit the string replacement could probably be optimized. But this is the best I came up with. Why are the paths still in " "? Because some executable paths have white spaces in it. I wish to avoid issues with the path parameter of Get-ACL later.

This leads to $services being declared like this:

PS > $services

(($_.PathName.replace('"',''))) -replace'(^.*\.exe).*','"$1"'
-------------------------------------------------------------
"C:\Windows\system32\svchost.exe"
"C:\Windows\System32\alg.exe"
"C:\Windows\system32\svchost.exe"
"C:\Windows\system32\svchost.exe"
"C:\Windows\system32\svchost.exe"
"C:\Windows\System32\svchost.exe"
"C:\Windows\system32\AppVClient.exe"

Here is my foreach loop and the error it throws:

PS > Foreach($s in $services){Get-ACL -Path $s }
Get-ACL : Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData' because it does not exist.
At line:1 char:26
+ Foreach($s in $services){Get-ACL -Path $s }
+                          ~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (:) [Get-Acl], ItemNotFoundException
    + FullyQualifiedErrorId : GetAcl_PathNotFound_Exception,Microsoft.PowerShell.Commands.GetAclCommand

I did try to run it with -ErrorAction SilentlyContinue as I first believed it was only the heading of $services that causes this issue. But if I do so, there is no output at all.
I did check what happens when I call Get-ACL -Path "C:\Windows\system32\svchost.exe" manually.
And that works:

    Directory: C:\Windows\system32


Path        Owner                       Access
----        -----                       ------
svchost.exe NT SERVICE\TrustedInstaller NT AUTHORITY\SYSTEM Allow  ReadAndExecute, Synchronize...

The output is not complete. But a | format-list at the end would fix that.

Can anyone give me some advice on how to get this path handled correctly? I am out of ideas.

Hi JC_D1984, welcome to the forum!

The biggest issue is the way you are using Select-Object. It’s designed to select objects when it seems you just want the strings. You can see in your output the line ---- that is differentiating the property from the property values. To do a proper calculated properties, you need to add a name/label in addition to the expression.

$services=Get-WmiObject win32_service | 
select @{Name='Executable';Expression={(($_.PathName.replace('"',''))) -replace'(^.*\.exe).*','"$1"'}}

Now you will have a list of objects with a single property ‘Name’
image

However, the current attempt at extracting the executable has several flaws.

  1. Not all services will have a value in PathName
  2. Not all will be .exe
  3. Adding " is not needed and counterproductive, as it doesn’t work well with Get-Acl

Here is what I suggest

$servicelist = Get-WmiObject win32_service | ForEach-Object {
    
    # If the 'executable' has any spaces, it will be double quoted, extract the value between them
    $path = if($_.pathname -match '^"([^"]+?)"'){
        $matches.1
    }
    else{
        # otherwise if pathname is not blank, split at the first space and take the first element, the executable path
        if($_.pathname){
            (-split $_.pathname)[0]
        }
    }

    [PSCustomObject]@{
        Name         = $_.name
        DisplayName  = $_.displayname
        OriginalPath = $_.pathname
        Path         = $path
    }
}

Now you will have a list of objects with the four properties we chose. The one you are after is Path (named as such for easier pipeline use). Keep in mind not all services will have a pathname (and subsequent executable/path), so we will filter those out. We can either pass to Get-Acl via pipeline or a foreach/ForEach-Object loop

Pipeline

$servicelist | Where-Object Path | Get-Acl

foreach statement loop

foreach($service in $servicelist | Where-Object Path){
    Get-Acl $service.path
}

ForEach-Object loop

$servicelist | Where-Object Path | ForEach-Object {Get-Acl $_.Path}

Now if we had decided to name Path something like Executable, it would’ve taken an extra step to pipeline

$servicelist = Get-WmiObject win32_service | ForEach-Object {
    
    # If the 'executable' has any spaces, it will be double quoted, extract the value between them
    $path = if($_.pathname -match '^"([^"]+?)"'){
        $matches.1
    }
    else{
        # otherwise if pathname is not blank, split at the first space and take the first element, the executable path
        if($_.pathname){
            (-split $_.pathname)[0]
        }
    }

    [PSCustomObject]@{
        Name         = $_.name
        DisplayName  = $_.displayname
        OriginalPath = $_.pathname
        Executable   = $path
    }
}

$servicelist | Where-Object Executable | Select-Object -ExpandProperty Executable | Get-Acl

#or 

$servicelist | Where-Object Executable | ForEach-Object Executable | Get-Acl

#or

($servicelist | Where-Object Executable).Executable | Get-Acl
2 Likes

Awesome - thank you. That works very well. And you are right about the issue with the objects in PowerShell. Lots of new things to take away here.

First of all that if-else construct.
I do get the syntax. But is $matches a standard variable I can use after working with -match “expression”? I would have expected $_ instead.
And what is the difference between $matches.1 and $matches[1]? Would (-split $_.pathname)[0] be the same as (-split $_.pathname).1?

Second take-away: the new object generated with its four properties. You make use of it and assign properties of the object being handled. Very neat.

One question regarding the three alternatives at the end to get the result to Get-ACL. Is there any difference between them? One that is more performant than the other maybe? Or is there are difference in internal handling?

And lastly: I would like to narrow done the selected services. From what I was told so far, I should do that narrowing down early on in the PS pipe. So do you see any issue with this:

 $servicelist = Get-WmiObject win32_service | Select-object Name, Displayname, Path |
Where-Object $_.Pathname -NotLike "C:\Windows\*" | ForEach-Object{

In this case I wish to exclude all services not running from somewhere within C:\Windows.

Did you try? If not, why not? :wink:

If performance matters for the task you’re trying to accomplish you should measure it. Often the approach with the foreach() loop is the most performant one.

Whatfor do you need the Select-Object then? :smirk:

I did run $matches.1 and $matches[1] and also tried it with the (-split $_.pathname).1. It does work. But I would like to know what the difference is there - if there is any?

With regard to the last part:
I did try running it without the Select-Object and it produced an error that measure didn’t get any properties to work with. So adding it solved that.

If you get errors you should share them COMPLETELY (formatted as code as well) along with the code you used.

Lots of information to unpack in these questions, I’ll try my best to summarize.

$matches is an automatic variable that powershell generally provides for you when working with regex operators (-replace, -match, -split) It is a normal hash table. You can access elements of a hash table by key, that’s what the 1 is doing here, it’s the first capture group named 1.

This is easier to understand if we name our capture group

$path = 'some test string'
$path -match '(?<NamedGroup>test) (?<Another>string)'
True

$matches

Name                           Value
----                           -----
Another                        string
NamedGroup                     test
0                              test string

You can see the two named groups plus the default unnamed group 0 when there is any match (the entire match)

You can access the values using either construct

$matches['Another']
$matches.Another

string
string

In older versions of powershell and some other weird instances I have yet to fully understand why, the dot notation method doesn’t work.

Now that we have that out of the way, that is different then

('a b c' -split ' ')[0]

a

which is selecting the first element in an array (0). That is why .0 didn’t work, cause it’s not member enumeration or property dot notation. When it comes to simple splitting at the space, you can put the split up front and powershell will assume you want to split at the spaces, making this the same as above

(-split 'a b c')[0]

a

Finally, for this

The select object is not only not helping you, it’s completely breaking what you’re trying to do as you remove the PathName property you attempt to filter on right after. The next issue is Where-Object can be used without curly braces if there is only one member being evaluated. In those instances, you do not provide $_. - you just give the property/method name.

# Invalid
Where-Object $_.Pathname -NotLike "C:\Windows\*"

# Valid
Where-Object {$_.Pathname -NotLike "C:\Windows\*"}
Where-Object Pathname -NotLike "C:\Windows\*"

The final issue that stands out is your wildcard pattern will only filter out those that do not start with a double quote, I’d change it to this in case there are pathname starting with a quote

$servicelist = Get-WmiObject win32_service |
    Where-Object Pathname -NotLike "*C:\Windows\*" |
        ForEach-Object { ... }
2 Likes

Thanks a lot for the explanation.