A Quick Remix

Shortly after I posted Powershell: Reporting on File Permissions, I was asked about how it could be done for remote target. Although in my case it easy to run it on the server I want to scan, in many organizations getting permissions to access servers directly, or getting the server team to implement scheduled scanning might not be possible. Thankfully, it only takes rather minor change to make the exact same script hit a target server.

The big gotcha would be if you have permissions or not to view a share, or files in a share. If you don’t have permissions, it will put an error on the console, but the reports won’t mention them, it will just skip right past. Maybe error reporting will be a future feature.

Enumerating Shares on a Remote Server

Looking online, I only found one simple, built in, reliable way to enumerate the shares on a remote server without needing WMI access to the target server. net view servername.

Shared resources at localhost



Share name  Type  Used as  Comment

-------------------------------------------------------------------------------
Test Share  Disk
Users       Disk
The command completed successfully.

It’s simple, but the output really stinks for automation. So let’s get that into a powershell useful output.

$ServerName = "localhost"
net view $ServerName | ForEach-Object {
    if($_.indexof(" Disk ") -gt 0) {
        $ShareName = ($_ -split "\s{2,}")[0]
    }
}

Let’s break that down. First we call net view as normal, but we pipe that directly in to a ForEach-Object, which loops through each line of the output as separate strings.

Once in a loop, we use indexof() to count how many times " Disk " shows up in the line. Note the spaces on both sides. That filters out all of the none share lines.

Next, we use -split to break the string into pieces. We are making an assumption, that’s not 100% certain, that none of your share names include any double spaces. \s{2,} tells split to look for 2 concurrent spaces. Because the output of net view always seems to put at least 2 spaces between Share name and Type. Once we have it all split up, we ask for the first result, which will always be share name. [0] might not be obviously the first part of the split up strings, but in programming, lists normally start at 0 instead of 1 the way humans normally think of it. So when you have a list on computers, you generally ask for 0 to get the first.

Alright, we have a nice output of every share on the machine now, that’s a great start. But how do we re-factor that back into my original script.

Well, if you look down low in my script, you will see it expects the $Share to have objects with 2 properties, Name and Path. So what we want to do is create an object out of the output of net view that replicates what we were using from Get-SmbShare. Thankfully we already have all the info we need, the share name and the server name are all we need to create an object that the rest of my script can use.

[PSCustomObject]@{
    'Name' = $ShareName
    'Path' = "\\$ServerName\$ShareName"
}

See, custom objects aren’t that hard. But how to we build that into the script? We put it in the ForEach-Object section.

$ServerName = "localhost"
net view "$ServerName" | ForEach-Object {
    if($_.indexof(" Disk ") -gt 0) {
        $ShareName = ($_ -split "\s{2,}")[0]
        [PSCustomObject]@{
            'Name' = $ShareName
            'Path' = "\\$ServerName\$ShareName"
        }
    }
}

So when that loops through, it is going to return just the custom object we built there. So right now, if we replace Get-SmbShare function, we’d pretty much have it working. But let’s modify the Where-Object part to account for it.

$ServerName = "localhost"
$Shares = net view "$ServerName" | ForEach-Object {
    if($_.indexof(" Disk ") -gt 0) {
        $ShareName = ($_ -split "\s{2,}")[0]
        [PSCustomObject]@{
            'Name' = $ShareName
            'Path' = "\\$ServerName\$ShareName"
        }
    }
} | Where-Object {$_.Name -ne "SYSVOL" -and $_.Name -ne "NETLOGON"}

And there we have it, that technically works. We don’t need the *$ part, because net view doesn’t return hidden shares. And we don’t need FileSystemDirectory, because we are already filtering to disk shares only, and ShareType isn’t a valid variable on the share.

Making it Easier to Run Against More Targets

The script works as is, but imagine you want to be able to set this up on a schedule, perhaps saving the logs to specific directories, targeting multiple servers. Perhaps even scanning from workstations or user accounts to see what they see. Well, you could have 20 different copies of the script, one for each server and save target, or you could include parameters on the script, which would allow you, when you run the PS1, to specify a target server and path to save the reports to. Thankfully, all we need to do is replace all the variables at the beginning with parameters we can pass data into.

param(
    [Parameter(Mandatory=$true)]
    [ValidatePattern('^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$')]
    [String] $ServerName,
    [Parameter(Mandatory=$false)][String] $OutputPath = ".",
    [Parameter(Mandatory=$false)][String] $OutputFileName = "$ServerName-Shares",
    [Parameter(Mandatory=$false)][Int] $RecurseDepth = 2
)

That’s going to force whoever is running it to supply a server name, but nothing else. But optionally the other things can be tweaked. I have added a simple validation pattern to the server name to prevent accidental errors that may result in very big problems (running unexpected commands on a command line). It’s not a 100% safeguard, I’m guessing, but it should prevent accidents.

Let’s put it all together now.

param(
    [Parameter(Mandatory=$true)]
    [ValidatePattern('^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$')]
    [String] $ServerName,
    [Parameter(Mandatory=$false)][String] $OutputPath = ".",
    [Parameter(Mandatory=$false)][String] $OutputFileName = "$ServerName-Shares",
    [Parameter(Mandatory=$false)][Int] $RecurseDepth = 2
)

#Automatically gets the list of shares. Excludes system shares and non-file shares like printers.
$Shares = net view "$ServerName" | ForEach-Object {
    if($_.indexof(" Disk ") -gt 0) {
        $ShareName = ($_ -split "\s{2,}")[0]
        [PSCustomObject]@{
            'Name' = $ShareName
            'Path' = "\\$ServerName\$ShareName"
        }
    }
}
#Gets NTDomain to help with name filters
$Domain = (Get-WmiObject Win32_NTDomain).DomainName

#List of names to filter out. Great for client facing reports, might not be best for change monitoring.
$NameFilter = "$Domain\MSPAdmin", 'BUILTIN\Administrators', 'NT AUTHORITY\SYSTEM', "$Domain\Domain Admins"


#Converts the Propagation and Inheritance flags into human readable output
Function Get-HumanReadablePropagation($PropagationFlags, $InheritanceFlags) {
    switch ($PropagationFlags) {
        "None" {
            switch ($InheritanceFlags) {
                "None" { return "This Folder Only" }
                "Container" { return "This Folder and Sub-folders" }
                "Object" { return "This Folder and Files" }
                Default { return "This Folder, Sub-folders and Files" }
            }
        }
        Default {
            switch ($InheritanceFlags) {
                "Container" { return"Sub-Folders" }
                "Object" { return "Files" }
                Default { return "Sub-folders and Files" }
            }
        }
    }
}
$Output = foreach ($Share in $Shares) {  
    $RootPath = $Share.Path
    $FolderPath = Get-ChildItem -Directory -Path $RootPath -Recurse -Depth $RecurseDepth -Force | Where-Object { $_.PSPath -notlike "*DfsrPrivate*" }
    $MainFolderACL = Get-Acl -Path $RootPath
    ForEach ($Access in $MainFolderACL.Access) {
        $Propagation = Get-HumanReadablePropagation($Access.PropagationFlags, $Access.InheritanceFlags)
        [PSCustomObject]@{
            'Share' = $Share.Name
            'Folder Name' = $RootPath
            'Group/User' = $Access.IdentityReference
            'Permissions' = $Access.FileSystemRights
            'Inherited' = $Access.IsInherited
            'Propagation' = $Propagation
        }
    }
    ForEach ($Folder in $FolderPath) {
        $Acl = Get-Acl -Path $Folder.FullName
        if ($Acl.Access.IsInherited -contains $false) {
            ForEach ($Access in $Acl.Access) {
                $Propagation = Get-HumanReadablePropagation($Access.PropagationFlags, $Access.InheritanceFlags)
                [PSCustomObject]@{
                    'Share' = $Share.Name
                    'Folder Name' = $Folder.FullName
                    'Group/User' = $Access.IdentityReference;
                    'Permissions' = $Access.FileSystemRights
                    'Inherited' = $Access.IsInherited
                    'Propagation' = $Propagation
                }
            }
         }
    }   
}

#Output Phase

$Output | Group-Object "Share" | ForEach-Object {
    $ShareOutput = $_.Group
    $FilteredOutput = $ShareOutput | Where-Object { $_."Group/User" -notin $NameFilter } 
    $FormattedOutput = $FilteredOutput | Format-Table "Group/User", "Permissions", "Inherited", "Propagation" -GroupBy "Folder Name"
    $FormattedOutput | Out-File "$OutputPath\$OutputFileName-$($_.Name).txt" -Width 120
    Write-Host "================`n$($_.Name)`n================"
    $FormattedOutput
}


$FilteredOutput = $Output | Where-Object { $_."Group/User" -notin $NameFilter } 
$FilteredOutput | Export-Csv -Path "$OutputPath\$OutputFileName.csv"