Share via


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

Happy St. Patrick’s Day!  Enjoy some PowerShell limericks here.  Download 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

  • Anonymous
    March 17, 2014
    Awesome post! I love the distribution type tip!
  • Anonymous
    March 18, 2014
    Hi,

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

    Please help

    Thanks in advance :)
  • Anonymous
    March 18, 2014
    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
  • Anonymous
    March 20, 2014
    Thanks for another great article. I love reading about these real-life challenges and solutions.
  • Anonymous
    September 24, 2014
    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...
  • Anonymous
    October 29, 2014
    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
  • Anonymous
    January 28, 2015
    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 <br>-Status $Path -PercentComplete ($i/$FolderCount*100)<br>$i&#43;&#43;<br><br># Get-ACL cannot report some errors out to the ErrorVariable.<br># Therefore we have to capture this error using other means.<br>Try {<br>$acl = Get-ACL -LiteralPath $folder -ErrorAction Continue<br>} Catch {<br>$ErrorLog &#43;= New-Object PSObject
    -Property @{CategoryInfo=$
    .CategoryInfo;TargetObject=$folder}
    }

    $acl.access | Where-Object {$.IsInherited -eq $false} | Select-Object <br>@{name='Root';expression={$path}},
    @{name='Path';expression={$folder}}, <br>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
  • Anonymous
    January 28, 2015
    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
  • Anonymous
    September 17, 2015
    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 <br>@{name='Root';expression={$path}},
    @{name='Path';expression={$folder}}, <br>@{Name='Group';Expression=<br>{<br>$Group = $_.IdentityReference -creplace '^[^\]*\', ''<br>$Group<br>}<br>},<br>@{Name='Group Members';Expression=<br>{<br>$GroupMember = Get-ADGroupMember $Group<br>$GroupMember.name -join &quot;,&quot;<br>}<br>},<br>#IdentityReference,<br>FileSystemRights,<br>IsInherited,
    InheritanceFlags,
    PropagationFlags
  • Anonymous
    September 25, 2015
    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.
  • Anonymous
    October 01, 2015
    The comment has been removed
  • Anonymous
    October 08, 2015
    A host of reference material for AD and Group Policy
  • Anonymous
    October 16, 2015
    The comment has been removed
  • Anonymous
    October 16, 2015
    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
  • Anonymous
    December 02, 2015
    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.
  • Anonymous
    December 07, 2015
    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
  • Anonymous
    January 12, 2016
    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.
  • Anonymous
    January 27, 2016
    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
  • Anonymous
    January 30, 2016
    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.
  • Anonymous
    February 11, 2016
    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!
  • Anonymous
    March 02, 2016
    Ashley, thank you very much!
  • Anonymous
    March 09, 2016
    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?
  • Anonymous
    March 16, 2016
    The comment has been removed
  • Anonymous
    August 29, 2016
    Hi. This works great, Thanks. Was wondering how I can run this so that it only outputs groups e.g.. "domain admins" "Accounts Dept" and not individual users such as "Bobby Joe" from the list re: from the output under column "Identityreference" I want to see just groups that are assigned to the shared folder. Many ThanksConfuseis
  • Anonymous
    September 03, 2016
    Hi On many though not all of folders I am getting the error belowGet-acl Cannot find path '\server\share' because it does not existI searched and seen some mention of get-acl not playing noce with the -literalpath parameterAnyone know a quick fix ?Thanks
  • Anonymous
    September 20, 2016
    Changed this part for powershell 5.0$subfolders = Get-Childitem $Path -Recurse -ErrorVariable +ErrorLog -ErrorAction SilentlyContinue | Where-Object {$_.PSIsContainer -eq $true} | Select-Object -ExpandProperty FullName to:$subfolders = Get-Childitem $Path -Recurse -Depth 5 -Directory -ErrorVariable +ErrorLog -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullNameNow it stops at a depth of 6 subfolders folders in path = 0 (we do not distribute rights after the third level, and this is the check) you can change this value if you want.
  • Anonymous
    December 09, 2016
    The comment has been removed
    • Anonymous
      January 05, 2017
      Per the comments above, Server 2008 R2 ships with PowerShell v2. You need to upgrade to the Windows Management Framework v3.
  • Anonymous
    January 11, 2017
    Is there a way to exclude certain default groups? eg NT AUTHORITY\Authenticated Users, NT AUTHORITY\SYSTEM, BUILTIN\Administrators, Everyonebecause im only after the security groups in AD that we have created that are used.
  • Anonymous
    February 17, 2017
    You can use robocopy instead to get around the 260 character path/filename limitation.
  • Anonymous
    March 09, 2018
    This is fantastic and the script works great. Thank you very much.
  • Anonymous
    April 07, 2018
    No matter if some one searches for his required thing, thus he/she needs to be available that in detail, thus that thing is maintained over here.
  • Anonymous
    March 21, 2019
    The comment has been removed