PowerShell to Find Where Your Active Directory Groups Are Used On File Shares


Happy St. Patrick’s Day!  Enjoy some PowerShell limericks hereDownload today’s script from the TechNet Script Gallery.

Where are my AD groups used?

Today's post gives you a script to crawl your file shares and document the AD users and groups referenced in NTFS permissions.  I’m sure others have published similar scripts, but I want to approach it from the angle of Active Directory group cleanup. Using this output together with the script from my last post will give you plenty of insight to go after stale groups.

Leprechaun

Finish this familiar quote, “I can’t delete that group, because ______________ .”  Multiple choice:

  • “I have no idea where it is used.”
  • “The last admin told me to never delete that group.”
  • “That is how the leprechauns get access.”
  • All of the above.

What would we do without file shares?  Well, actually, we would use SharePoint or OneDrive. The truth is file shares have been around for decades, and in most cases mission critical data resides there.  But who can access that data?  That is the big question, and many of us cannot give a complete answer.

By the way, if you would like a security report for SharePoint group usage, my peer, Brian Jackett, has a script for that.  (That sentence had more commas than a CSV file.)

The Solution

Our solution today involves two scripts:

  • Get Access Control Entries.  This script scans file server paths provided by an input text file.  The text file simply lists the root UNC path to every share you want to scan. It exports a CSV report of all explicitly defined (not inherited) permissions at the folder level recursively down a file share path.
  • Merge CSV NTFS Scans.  This script combines all of the individual CSV permission reports into a single file for importing into a database.

In my SID history series I included a function to scan file shares for SID history and migrate the NTFS ACL entries to the new SID.  Basically I retooled that code to simply report all access and ignore SID history. This time, however, I used the Access property instead of the SDDL property.  I recommend that you read this particular post for more background information.

The Code

This script is really not that complicated, so it will be a good one to study if you’re learning PowerShell.  The main cmdlet is Get-ACL.  Everything else is loops, error checking, and progress bars.

First, populate paths.txt with local drive paths and/or UNC paths for the root of each share to scan. For each path in the file you will get two CSV output files:

  • ACEs – An exhaustive list of every user or group explicitly assigned permissions at the folder level all the way down the tree.
  • Errors – Here you will find the folder paths with error messages encountered during the scan.  Popular errors include Access Denied and Path Too Long.

Be sure to review the error log for each share scanned.  You may need to run another scan with different credentials.

#Requires -Version 3.0            
            
Function Get-ACE {            
Param (            
        [parameter(Mandatory=$true)]            
        [string]            
        [ValidateScript({Test-Path -Path $_})]            
        $Path            
)            
            
    $ErrorLog = @()            
            
    Write-Progress -Activity "Collecting folders" -Status $Path `
        -PercentComplete 0            
    $folders = @()            
    $folders += Get-Item $Path | Select-Object -ExpandProperty FullName            
 $subfolders = Get-Childitem $Path -Recurse -ErrorVariable +ErrorLog `
        -ErrorAction SilentlyContinue |             
        Where-Object {$_.PSIsContainer -eq $true} |             
        Select-Object -ExpandProperty FullName            
    Write-Progress -Activity "Collecting folders" -Status $Path `
        -PercentComplete 100            
            
    # We don't want to add a null object to the list if there are no subfolders            
    If ($subfolders) {$folders += $subfolders}            
    $i = 0            
    $FolderCount = $folders.count            
            
    ForEach ($folder in $folders) {            
            
        Write-Progress -Activity "Scanning folders" -CurrentOperation $folder `
            -Status $Path -PercentComplete ($i/$FolderCount*100)            
        $i++            
            
        # Get-ACL cannot report some errors out to the ErrorVariable.            
        # Therefore we have to capture this error using other means.            
        Try {            
            $acl = Get-ACL -LiteralPath $folder -ErrorAction Continue            
        }            
        Catch {            
            $ErrorLog += New-Object PSObject `
                -Property @{CategoryInfo=$_.CategoryInfo;TargetObject=$folder}            
        }            
            
        $acl.access |             
            Where-Object {$_.IsInherited -eq $false} |            
            Select-Object `
                @{name='Root';expression={$path}}, `
                @{name='Path';expression={$folder}}, `
                IdentityReference, FileSystemRights, IsInherited, `
                InheritanceFlags, PropagationFlags            
            
    }            
                
    $ErrorLog |            
        Select-Object CategoryInfo, TargetObject |            
        Export-Csv ".\Errors_$($Path.Replace('\','_').Replace(':','_')).csv" `
            -NoTypeInformation            
            
}            
            
# Call the function for each path in the text file            
Get-Content .\paths.txt |             
    ForEach-Object {            
        If (Test-Path -Path $_) {            
            Get-ACE -Path $_ |            
                Export-CSV `
                    -Path ".\ACEs_$($_.Replace('\','_').Replace(':','_')).csv" `
                    -NoTypeInformation            
        } Else {            
            Write-Warning "Invalid path: $_"            
        }            
    }            

 

Disclaimers

  • This will likely take hours or days to run depending on the size of your shares.
  • You must run this script from PowerShell v3 or later.
  • Paths longer than 260 characters will error.
  • You must run the script with permissions to read all of the folders down the file share tree.
  • In order to keep the script as efficient as possible we do not scan individual file permissions.
  • This script does not look at the share permissions, only NTFS. In my field experience most places use Everyone/FullControl on their share roots and manage permissions with NTFS.

 

Roll ‘em Up

I included a bonus script that will merge all of the CSV output. This is rather short and sweet. It just saves you the time of doing it yourself. The result is a file called NTFSScan.CSV containing all of the CSV output rolled into one file.

The Next Level

Now that you have this rich group data in CSV format you can pull it all into a database for analysis. In the past I have used Microsoft Access for a quick proof-of-concept.  I pulled in the group report, group duplication report, and the merged NTFS permission CSV output. (You could even pull in the AD organizational unit permission report.) I imported these from CSV to new Access tables.  Then I created some queries that relate the data and report on things like:

  • Perfect match group memberships at 100%
  • Group counts by category and scope
  • Empty groups not updated in 12 months
  • Groups not used in NTFS permissions
  • Pivot table (cross tab) report of groups used on each server
  • Summary of groups used in NTFS permissions
  • Etc.

These reports will give you insight into the use of groups in your environment. You can also see where users are assigned permissions directly instead of using groups.

Group Cleanup

There are many factors that go into group cleanup.  Just because a group has not been updated in over one year does not always mean it is stale, especially for some of the built-in AD groups. Groups are used in so many places across the enterprise that it is nearly impossible to say that one is not in use at all. However, when combined with usage data like we collected with today’s script, we can get a far more accurate list of which groups are potentially stale. Go here for a list of other group cleanup posts.

Pro Tip: Instead of deleting a global group right away try this: change the group type to Distribution group. That will effectively remove it as a security group. That may be enough of a fail safe that you can flip it back to Global group should the need arise. If no one calls in the next 30 days, then there is a possibility you could completely delete it.

Pro Tip: When it comes time to clean up your groups make sure you have the AD Recycle Bin turned on and a full backup of your Active Directory.

With proper caution and investigation you should now have a good start on stale group cleanup. Happy hunting!

Download the full script from the TechNet Script Gallery.

Comments (23)

  1. Ashley McGlone says:

    Hi Varinder,
    You are probably trying to run it in PowerShell v2. You will need PowerShell v3 or newer. Get-ACL (and several other cmdlets) have a new parameter LiteralPath that avoids some issues with odd characters when using the Path parameter. Install PowerShell v3 or v4 and try again. You can go to download.microsoft.com and search for WMF Windows Management Framework for the download.
    Hope this helps,
    Ashley @GoateePFE

  2. Joseph__Moody says:

    Awesome post! I love the distribution type tip!

  3. Ashley McGlone says:

    Hi Brian,
    Great feedback. Actually I have updated this code to simply use the new -Directory parameter in PS v3 and above. Then we skip all the files without piping to a where-object, and it goes much faster. I have updated this a while back in my SIDHistory module.
    http://aka.ms/SIDHistory
    Thanks,
    Ashley
    GoateePFE

  4. Varinder Singh Vashisht says:

    Hi,

    While running the script I am getting error “InvalidArgument: (:) [Get-Acl], ParameterBindingException”

    Please help

    Thanks in advance :)

  5. Bjorn Houben says:

    Thanks for another great article. I love reading about these real-life challenges and solutions.

  6. Cole says:

    How can I limit the depth to 4 sub-folders? I saw something along the lines of this: Get-ChildItem *** but I am not sure how to implement that since the command is using a variable for the patch filter…

  7. Anonymous says:

    Welcome! Today’s post includes demo scripts and links from the Microsoft Virtual Academy event: Using PowerShell for Active Directory . We had a great time creating this for you, and I hope you will share it with anyone needing to ramp up their

  8. I have used your script for pre-migration work, thanks a lot, it works well for what I need which is to just find all the possible AD groups used to secure data on the server. I really don’t care about where each ACL is used, I just need a unique list
    of groups in use.

    I had a problem that on very large volumes the Get-ChildItem process to get the list of folders is REALLY slow. This is because gci calculates a full .NET object for each file and folder when we only really need the full path of folders. It is about 1000 times
    faster to get the recursive list of folder names using the old DOS "DIR" command. Again, this also does not support long paths in most cases the really deep structures are inheriting permissions which makes little to no difference for what I am collecting.

    So, here is what I did to increase the speed of the script.

    Create a batch file "GetDirectoryNames.cmd" and save it in C:scripts. It is a simple script containing only two lines.

    @dir /ad /s /b %1
    @exit

    The PowerShell script was modified to the following:

    #Requires -Version 3.0
    Function Get-ACE {
    Param (
    [parameter(Mandatory=$true)]
    [string]
    [ValidateScript({Test-Path -Path $_})]
    $Path
    )

    $ErrorLog = @()

    Write-Progress -Activity "Collecting folders" -Status $Path -PercentComplete 0
    $folders = c:scriptsGetDirectoryNames.cmd $Path
    Write-Progress -Activity "Collecting folders" -Status $Path -PercentComplete 100

    # We don’t want to add a null object to the list if there are no subfolders
    If (-not $folders) {$folders += $Path}
    $i = 0
    $FolderCount = $folders.count

    ForEach ($folder in $folders) {

    Write-Progress -Activity "Scanning folders" -CurrentOperation $folder `
    -Status $Path -PercentComplete ($i/$FolderCount*100)
    $i++

    # Get-ACL cannot report some errors out to the ErrorVariable.
    # Therefore we have to capture this error using other means.
    Try {
    $acl = Get-ACL -LiteralPath $folder -ErrorAction Continue
    } Catch {
    $ErrorLog += New-Object PSObject `
    -Property @{CategoryInfo=$_.CategoryInfo;TargetObject=$folder}
    }

    $acl.access | Where-Object {$_.IsInherited -eq $false} | Select-Object `
    @{name=’Root’;expression={$path}}, `
    @{name=’Path’;expression={$folder}}, `
    IdentityReference, FileSystemRights, IsInherited, `
    InheritanceFlags, PropagationFlags
    }

    $ErrorLog | Select-Object CategoryInfo, TargetObject `
    | Export-Csv ".Errors_$($Path.Replace(”,’_’).Replace(‘:’,’_’)).csv" -NoTypeInformation
    }

    Get-ACE -Path "E:" | select -expand IdentityReference | sort -Unique | out-file c:scriptsE_Drive.txt

  9. Matt Salzmann says:

    Thank you Ashley! This script as come in very handy, as we are preparing for internal and external security audits. I modify the Select-Object section slightly to gather the members of the reported groups as well:

    $acl.access |
    Where-Object {$_.IsInherited -eq $false} |
    Select-Object `
    @{name=’Root’;expression={$path}}, `
    @{name=’Path’;expression={$folder}}, `
    @{Name=’Group’;Expression=
    {
    $Group = $_.IdentityReference -creplace ‘^[^\]*\’, ”
    $Group
    }
    },
    @{Name=’Group Members’;Expression=
    {
    $GroupMember = Get-ADGroupMember $Group
    $GroupMember.name -join ","
    }
    },
    #IdentityReference,
    FileSystemRights,
    IsInherited, `
    InheritanceFlags,
    PropagationFlags

  10. Vitek says:

    First, I want to say thank you for the script. It works very good, but I do have a slight problem with it and wanted to see if anyone can help. Our global security groups are populated into the local groups on the server which then assigned into NTFS permissions
    of the share. When the scan finishes, it will displays SID’s instead of the group names.

    I performed a little test and populated one of the shares with global security group instead of the local group and when the scan ran, it did logged the correct group name against AD.

    Is there any way to make a change in the script to log local groups by their names instead of SID’s?

    Thank you.

  11. Raj says:

    Hi Ashley, great script. We will use your script for AD group cleanup project, thanks a lot,

    We have a list of Groups we know we want to delete and I just wanted to write a line to the out file if the group is in the list. Where do I put this check?

    Sorry I am very new to PowerShell and learning.

    Thanks,

  12. Anonymous says:

    A host of reference material for AD and Group Policy

  13. Hello Raj,
    You could use "Compare-Object -IncludeEqual -ExcludeDifferent" to compare the CSV output IdentityReference column to the contents of your text file listing groups to delete, and then pipe that to "Remove-ADGroup -WhatIf". That should get your started.
    Thanks,
    Ashley
    GoateePFE

  14. Hello Vitek,
    You could query the local group names using an ADSI connection to WinNT on the local SAM database on each server. You could add that to this script, or create a second script to process this output and append group names.
    Thanks,
    Ashley
    GoateePFE

  15. Chris says:

    Hi Ashley,

    Great script! I did run into one issue though. When running against folder shares, the script failed at the point it reached a share with a space in the name. I.E.

    \shareAfolder
    \shareBfolder
    \shareCfolder

    worked fine but

    \shareD folder

    did not.

    Is there an easy fix to this?

    Also, I would recommend noting somewhere in your article that while this script and the pro-tip to change to a distribution group temporarily (great idea!) go a long way, there are still areas where security groups might be used that have not been addressed.
    I’m thinking specifically item-level targeting on a GPO or printer security settings.

  16. Hello Chris,
    Can you paste in an example of the error you get? Please change any internal names before pasting into your comment.
    Thanks,
    GoateePFE

  17. Prashant says:

    Hi Ashley, Just i want to find out the "one group is permissioed on how many folder path" results should be included with inherited permission as well.

  18. NeighborGeek says:

    Ashley — When I run the script, everything seems to be working, but I come back to the console after a few hours to find the error below. I do have 2 csv files, ACEs_e__.csv and Errors_e__.csv, but when I open the ACEs file it only contiains one column
    "Length". Any idea where I’m going wrong?

    The script failed due to call depth overflow.
    At C:tempget-aceget-ace.ps1:42 char:2
    + $subfolders = Get-Childitem $Path -Recurse -ErrorVariable +ErrorLog `
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (0:Int32) [], RuntimeException
    + FullyQualifiedErrorId : CallDepthOverflow

  19. NeighborGeek says:

    Found my issue… several levels down in the folder structure, there was a folder with a blank filename. Open that in explorer, and it shows… a folder with a blank name, which then contains a folder with a blank name. . .

    running "gci -recurse" on the parent directory presents a list of blank folders until powershell crashes.

    After moving that folder outside of the structure I needed permissions for, I ran the script again and it bombed at a different place, on another folder with a special name, "com3". Having cleaned that one up, I’m running the script again now. Isn’t it interesting,
    the things that turn up when you dig through every folder on a large fileshare that has been around for a very long time?

    For anyone else running into similar issues in the future, I stumbled across the problem folders when I dot sourced the script and then run the get-ace function manually against a specific path. This let me see a lot of the output from get-ace, which otherwise
    was hidden. I could see that just before the error, the script had listed hundreds of copies of the same directory path (ending in the folder with the blank name.)

    Hopefully the script finishes successfully this time, but if not, at least I know how to find what it’s choking on now.

  20. Benjamin says:

    Is there any way to limit the search? Like only going 4 levels down in each share? Running it on my companys fileshares takes ages when doing a full scan. Thanks for the great code!

  21. John says:

    Ashley, thank you very much!

  22. Ed_Dowling says:

    Love this script. although my experience in PS is very limited and I have been handed a task by my supervisor to remove groups in AD that are not currently being used.

    My problem is I am getting [Get-ChildItem], PathTooLongException on many records.

    Most are not over the 260 Character limit.

    Anychance you can help with this?

  23. RickyHolland says:

    Hello Ashley,

    Hope you don’t mind but I’ve slightly ammended the provided script to use the NTFSSecurity module (https://ntfssecurity.codeplex.com/). This should now allow the script to handle 256+ character paths.

    Currently still testing but seems to be working fine from prelinary results.

    Import-Module NTFSSecurity

    Function Get-ACE
    {
    Param (
    [parameter(Mandatory = $true)]
    [string][ValidateScript({ Test-Path -Path $_ })]
    $Path
    )

    $ErrorLog = @()

    Write-Progress -Activity “Collecting folders” -Status $Path `
    -PercentComplete 0
    $folders = @()
    $folders += Get-Item2 $Path | Select-Object -ExpandProperty FullName
    $subfolders = Get-ChildItem2 $Path -Recurse -ErrorVariable +ErrorLog |
    Where-Object { ($_.GetType()).Name -eq “DirectoryInfo” } |
    Select-Object -ExpandProperty FullName
    Write-Progress -Activity “Collecting folders” -Status $Path `
    -PercentComplete 100

    # We don’t want to add a null object to the list if there are no subfolders
    If ($subfolders) { $folders += $subfolders }
    $i = 0
    $FolderCount = $folders.count

    ForEach ($folder in $folders)
    {

    Write-Progress -Activity “Scanning folders” -CurrentOperation $folder `
    -Status $Path -PercentComplete ($i/$FolderCount * 100)
    $i++

    Try
    {
    $acl = Get-NTFSAccess -Path $folder
    }
    Catch
    {
    $ErrorLog += New-Object PSObject `
    -Property @{ CategoryInfo = $_.CategoryInfo; TargetObject = $folder }
    }

    $acl |
    Where-Object { $_.IsInherited -eq $false } |
    Select-Object `
    @{ name = ‘Root’; expression = { $path } }, `
    @{ name = ‘Path’; expression = { $folder } }, `
    Account, AccessRights, IsInherited, `
    InheritanceFlags, PropagationFlags

    }

    $ErrorLog |
    Select-Object CategoryInfo, TargetObject |
    Export-Csv “.\Errors_$($Path.Replace(‘\’, ‘_’).Replace(‘:’, ‘_’)).csv” `
    -NoTypeInformation

    }

    # Call the function for each path in the text file
    Get-Content “.\paths.txt” |
    ForEach-Object {
    If (Test-Path -Path $_)
    {
    Get-ACE -Path $_ |
    Export-CSV `
    -Path “.\ACEs_$($_.Replace(‘\’, ‘_’).Replace(‘:’, ‘_’)).csv” `
    -NoTypeInformation
    }
    Else
    {
    Write-Warning “Invalid path: $_”
    }
    }

    Best Regards,

    Ricky.