TimeLoad
TimeLoad

Reputation: 371

Unsure About Get-ChildItem Result Format

I am trying to write a script that will iterate over every registry key and check their permissions. I have created a test registry key that does meet all of the requirements and should get printed to the console, but whenever I run this script, I keep getting lots of blank lines. I believe that it's the way I'm trying to iterate over the registry keys and the format it's returning.

$ErrorActionPreference = "SilentlyContinue"

Get-ChildItem -LiteralPath "HKLM:\" -Recurse | % {
  $output = Get-Acl $_.Name | format-list | Out-String
  $output = $output -split "`r`n"

  ForEach ($line in $output) {
    If ($line -contains "         BUILTIN\Users Allow  FullControl") {
      echo $_.Name
    }
  }
}

This following script echoes "success" validating part of the code:

$output = Get-Acl "HKLM:\SOFTWARE\testkey" | format-list | Out-String
$output = $output -split "`r`n"

ForEach ($line in $output) {
  If ($line -contains "         BUILTIN\Users Allow  FullControl") }
    echo "success"
  }
}

I expect that the first script will iterate over all the HKLM keys and return "testkey", but it echoes heaps of empty lines to the console.

Upvotes: 2

Views: 759

Answers (1)

briantist
briantist

Reputation: 47872

There are a number issues I see with the code as is, but first I'll get to the point of why you're seeing so may blank lines.

When you use $output = Get-Acl $_.Name let's take a moment to understand what $_ represents. It is the "current item" being processed by ForEach-Item (you're using its alias %), which in this case is a registry item being returned from Get-ChildItem.

This object contains a property .Name which you're referencing, but if you look at that property value for it, you'll see that it's a registry key name, as regedit tends to reference it, i.e. HKEY_LOCAL_MACHINE\Key, which is not a way you can call it in PowerShell.

When you call Get-Acl $_.Name then, Get-Acl has no idea what you're looking for. Notice how in your "success" example, you gave a fully qualified path to the registry key using HKLM:\; this is a PSDrive which references the PSProvider for browsing the registry similarly to a filesystem and its full name is Microsoft.PowerShell.Core\Registry, making the full path to your item: Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Key. Yuck!

The reason you didn't see this error by the way is because you set $ErrorActionPreference to SilentlyContinue, so the errors were ignored and not sent to the screen. You should usually not have that set.

Instead of trying to parse and/or concatenate strings though, take a look at your registry key object's full set of properties and see if there's something you can use:

Get-ChildItem HKLM:\ -Recurse | Select -First 1 | Format-List *

As ha! There's a property called PSPath that looks like it has the fully qualified path name!

Get-ChildItem HKLM:\ -Recurse | Select -First 1 | % { Get-Acl $_.PSPath }

Success! (see below for a more pipeline-y way to do it)


So now I'm going to take a slight detour and talk about a few other things. I know, it's long-winded, but bear with me.

First, is your use of | Format-List | Out-String. It's common when starting out with PowerShell to look at the output of commands and look at their format, much as you'd do with other shells, for figuring out how to parse or process or otherwise deal with the information given.

This is almost always a mistake. One of the main features of PowerShell is its heavy use of full objects being passed around between commands. This means the are full .Net classes with properties and often methods.

It's very important to realize that what you see on the screen, is just a formatted rendering of the underlying object. It may not show everything, it may be truncated, and most importantly, it doesn't need to be parsed as text.

Back to your usage, keep this in mind when using any Format- command: it's intended to change the display format of an object on the screen. It irreversibly loses the original object's data, and should really only be used for display purposes, usually by a user at a console as opposed to in a script.

If you looked at the output of Get-Acl, you might have thought it was easiest to parse as text, but remember, it's an object. Instead, take a look at this approach; you can run this from PowerShell in any directory with some items in it:

$acl = Get-ChildItem HKLM:\ -Recurse | Select-Object -First 1 -ExpandProperty PSPath | Get-Acl

(notice the difference from before, I kept it in the pipeline, used Select-Object to give me just one property, then piped it right into Get-Acl, without neededing to iterate with ForEach-Object)

Now look at $acl. You'll see it probably in table format, kind of truncated, so you might look at it again with $acl | Format-List and see more of the properties it contains. Use this output to guide your coding. Your script is looking for an entry in the "Access" list, so try this:

$acl.Access

Now, you see a list of new objects, each with their own properties. These are access rules, and they contain individual properties corresponding to what you need. It's an array of them, so you can filter through them using a command called Where-Object (you may have seen its aliases, Where or ?), but first, inspect those objects:

$acl.Access[1].IdentityReference
# ^ looks good for getting the identity you want to compare against

$acl.Access[1].RegistryRights
# ^ this one's tricky, looks like a string, but it's not
# check its type and other members

$acl.Access[1].RegistryRights | Get-Member

Looks like .RegistryRights is its own object of type System.Security.AccessControl.RegistryRights but it doesn't seem to have useful properties. You might also notice some values for this property look like comma separated strings.

This is an enum (enumeration), and it's basically just a bunch of numbered values that have names to them so they're more meaningful in code. This particular type is a bitfield and can have multiple values set at one time.

Quick tip for looking at types in the console, really helpful for enums: type names (classes) are represented in PowerShell in square brackets [], so like, [string] for example, or fully qualified [System.String]. You access members of the type itself with :: rather than .. You can tab complete types easily, so start typing: [RegistryRigh then hit TAB, now type :: and hit TAB again. You can start to cycle through the values.

Since this type can have multiple values, you can use a convenient method supplied by the enum called HasFlag (which you may have seen in the output of Get-Member) to determine whether it contains a particular value.

$acl.Access[1].RegistryRights.HasFlag([System.Security.AccessControl.RegistryRights]::FullControl)

I'm going to gloss over it, but .Access.AccessControlType is the same thing: an enum.

Now you can put this together with the identity to find what you want:

$UsersFullControlEntries = $acl.Access | Where-Object -FilterScript {
    $_.IdentityReference -eq 'BUILTIN\Users' -and 
    $_.RegistryRights.HasFlag([System.Security.AccessControl.RegistryRights]::FullControl) -and
   $_.AccessControlType.HasFlag([System.Security.AccessControl.AccessControlType]::Allow)
}

$UsersFullControlEntries now contains each entry that matches your condition (which should be 1 or 0 entries, but it won't matter if it's more than 1).


Let's put it together with original code:

$desiredRights = [System.Security.AccessControl.RegistryRights]::FullControl
$desiredControlType = [System.Security.AccessControl.AccessControlType]::Allow
# this is helping formatting

Get-ChildItem -LiteralPath "HKLM:" -Recurse | % {
    $acl = $_.PSPath | Get-Acl  
    $UsersFullControlEntries = $acl.Access | Where-Object -FilterScript {
        $_.IdentityReference -eq 'BUILTIN\Users' -and 
        $_.RegistryRights.HasFlag($desiredRights) -and
        $_.AccessControlType.HasFlag($desiredControlType)
    }

    if ($UsersFullControlEntries) {
        echo $_.Name
    }
}

But there's more we can do here. Your whole iteration is all about finding items that meet a certain criteria, and as we saw earlier, that's exactly what Where-Object does! Its scriptblock, instead of returning output, should return a boolean $true/$false value, and if the value is $true, then the original object ($_) will be passed down the pipeline. So we could replace your ForEach-Object with Where-Object for a nicer syntax:

$desiredRights = [System.Security.AccessControl.RegistryRights]::FullControl
$desiredControlType = [System.Security.AccessControl.AccessControlType]::Allow
# this is helping formatting

Get-ChildItem -LiteralPath "HKLM:" -Recurse | Where-Object {
    $acl = $_.PSPath | Get-Acl  
    $acl.Access | Where-Object -FilterScript {
        $_.IdentityReference -eq 'BUILTIN\Users' -and 
        $_.RegistryRights.HasFlag($desiredRights) -and
        $_.AccessControlType.HasFlag($desiredControlType)
    }
}

$UsersFullControlEntries isn't needed now, that value gets returned directly from the scriptblock, being directly interpreted as true/false, without the need for you to write a conditional, and now the original object is returned to the caller (not the .Name property), but this is better; you should strive to preserve that, so that you can use the fullest object for as long as possible. At any point you can reference or expand the .Name property of the returned object, but you also have all the other properties at your disposal, if you need them.

One more optimization, let's get rid of that $acl variable now and just make it one pipeline inside the Where-Object:

$desiredRights = [System.Security.AccessControl.RegistryRights]::FullControl
$desiredControlType = [System.Security.AccessControl.AccessControlType]::Allow
# this is helping formatting

Get-ChildItem -LiteralPath "HKLM:" -Recurse | Where-Object {
    $_.PSPath | 
        Get-Acl | 
        Select-Object -ExpandProperty Access | 
        Where-Object -FilterScript {
            $_.IdentityReference -eq 'BUILTIN\Users' -and 
            $_.RegistryRights.HasFlag($desiredRights) -and
            $_.AccessControlType.HasFlag($desiredControlType)
        }
}

Hope this gives you some insight and ideas and tools to continue exploring PowerShell!

Upvotes: 2

Related Questions