Introduction
File Server Audit solves the problem of getting raw data to find what a user can access on a large file server by enumerating all NTFS (New Technology File System) ACLs (Access Control Lists) for all folders. With text utilities or SQL queries, the raw data can be turned into useful reports to find what a user has access to. The File Server View also can be used to help look at the output. File Server Audit will help someone else by allowing them to quickly create their own auditing process for their file servers. The code snippet is a sub to read Reads Discretionary Access Control Lists (ACLs). This helps when looking for who has access to folders.
Screenshot

Background
I wrote this as part of a file server migration and cleanup project, to tell me all who had access to files and folders. This program was really helpfully at showing all the different people that had access to folders.
Using the Code
The code works by enumerating every folder on a volume and then enumerating each user and group that has access to each folder. The program will then take the groups and enumerate all users in that group and all users in nested groups. By doing this, it will give you a full list of users that have access to a file or folder. In the code snippet is the sub to enumerate all ACLs (Access Control Lists) for a folder. This code needs to be run within a Domain account so that the proper enumeration can be done. There is another sub called RecursiveGroupSub
that enumerates nested groups in the program source. Call the sub and give it a valid path to a folder. This program does output a lot of data which is hard to sort and analyze.
Global variables; uses Dictionaries as local cache to speed up AD records.
Dim blnShowOutput As Boolean = True
Dim blnInherited As Boolean = False
Dim strDate As String = TodayFileDate()
Dim dicADUserType As New Dictionary(Of String, String)
Dim dicADGroup As New Dictionary(Of String, Array)
Dim dicADGroupManager As New Dictionary(Of String, String)
Dim dicADGroupSubGroup As New Dictionary(Of String, Array)
Setup for the ReadACLs
sub that reads the actual ACLs. In the setup, DirInfo
binds to the input directory so that DirSec
can get a list of ACLs. This list is then set in the object ACLs.
Public Sub ReadACLs(ByVal strInput As String, _
Optional ByVal blnRemoveInherited As Boolean = True)
Dim DirInfo As System.IO.DirectoryInfo = _
New System.IO.DirectoryInfo(strInput)
Dim DirSec As System.Security.AccessControl.DirectorySecurity = _
DirInfo.GetAccessControl()
Dim ACLs As System.Security.AccessControl.AuthorizationRuleCollection = _
DirSec.GetAccessRules(True, True, _
GetType(System.Security.Principal.NTAccount))
Dim ACL As System.Security.AccessControl.FileSystemAccessRule
Dim Owner As System.Security.Principal.IdentityReference = _
DirSec.GetOwner(GetType(System.Security.Principal.NTAccount))
Dim DomainContext As New _
System.DirectoryServices.ActiveDirectory.DirectoryContext(_
System.DirectoryServices.ActiveDirectory.DirectoryContextType.Domain)
Dim objMember As New System.DirectoryServices.DirectoryEntry
Dim arrDUSplit(1) As String
Dim strTemp As String = ""
Dim strSAMA As String = ""
Dim strClass As String = ""
Dim strGroupSAMA As String = ""
Dim strManagedBy As String = ""
Dim strOwner As String = Owner.ToString
Dim StrACLIdentityReference As String = ""
Dim StrInheritanceFlags As String = ""
Dim StrFileSystemRights As String = ""
Dim StrIsInherited As String = ""
Dim StrArrayLoopSubGroup As String
Dim StrArrayLoopUser As String
On Error GoTo ErrorHandle
Now we loop though all the ACLs, since each folder can have many ACLs. We also have to deal with all local user, machine, and deleted accounts. Deleted accounts show up as just the SID (S-1-5-21-...). Once we have an ACL, we write out all the properties we want to a LogWrite
that will write out the record to log, SQL, or screen.
For Each ACL In ACLs
StrACLIdentityReference = ACL.IdentityReference.ToString
StrInheritanceFlags = ACL.InheritanceFlags.ToString
StrFileSystemRights = ACL.FileSystemRights.ToString
StrIsInherited = ACL.IsInherited.ToString
If Str.Left(StrACLIdentityReference, 2) = "S-" Then
strSAMA = "Unknown"
strGroupSAMA = StrACLIdentityReference
strManagedBy = "Unknown"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Else
arrDUSplit = Split(StrACLIdentityReference, "\")
Select Case arrDUSplit(0)
Case "BUILTIN"
strSAMA = arrDUSplit(1)
strGroupSAMA = "System"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case "NT AUTHORITY"
strSAMA = arrDUSplit(1)
strGroupSAMA = "System"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case "CREATOR OWNER"
strSAMA = arrDUSplit(0)
strGroupSAMA = "System"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case "Everyone"
strSAMA = arrDUSplit(0)
strGroupSAMA = "Everyone"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case Environment.MachineName.ToString
strSAMA = arrDUSplit(1)
strGroupSAMA = "Local"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Below is how we deal with domain accounts. First we check to see if the record is in cache; if not, we query AD and add it to the cache. After we find out what type the account is, we then can call LogWrite
if it is a user or computer account, otherwise we enumerate the group. Now we check if the group is in the cache; if it is, we use the cache. However, if a sub-group is not in the cache, we call RecursiveGroupSub
to get all the members of that group and any sub-groups; then we add those to the cache.
Case Environment.UserDomainName.ToString
If dicADUserType.ContainsKey(arrDUSplit(1)) Then
strSAMA = arrDUSplit(1)
strClass = dicADUserType(arrDUSplit(1)).ToString
Else
objMember = GetUser(arrDUSplit(1))
If objMember Is Nothing Then
CurrentRecord_Event("Error Number: " & Err.Number & " " & Err.Description & vbCrLf _
& " Sub GetUser: " & vbCrLf _
& vbTab & "DirInfo.FullName: " & DirInfo.FullName & vbCrLf _
& vbTab & "NTFS ACL: " & StrACLIdentityReference & vbCrLf _
& vbTab & "Domain: " & arrDUSplit(0) & vbCrLf _
& vbTab & "User: " & arrDUSplit(1) & vbCrLf)
Else
strClass = objMember.SchemaClassName.ToString
strSAMA = objMember.Properties("sAMAccountName").Value.ToString
dicADUserType.Add(strSAMA, strClass)
End If
End If
Select Case strClass
Case "user", "computer"
strGroupSAMA = "Direct"
strManagedBy = "Systems Administrators " & Environment.UserDomainName.ToString & " Domain Accounts"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case "group"
If dicADGroup.ContainsKey(arrDUSplit(1)) Then
strManagedBy = ""
If dicADGroupManager.ContainsKey(arrDUSplit(1)) Then strManagedBy = dicADGroupManager(arrDUSplit(1))
For Each StrArrayLoopUser In dicADGroup(arrDUSplit(1))
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
If dicADGroupSubGroup.ContainsKey(arrDUSplit(1)) Then
For Each StrArrayLoopSubGroup In dicADGroupSubGroup(arrDUSplit(1))
strManagedBy = ""
If dicADGroupManager.ContainsKey(StrArrayLoopSubGroup) Then strManagedBy = dicADGroupManager(arrDUSplit(1))
If Not dicADGroup.ContainsKey(StrArrayLoopSubGroup) Then
If Not RecursiveGroupSub(objMember) = StrArrayLoopSubGroup Then GoTo ErrorHandle
End If
For Each StrArrayLoopUser In dicADGroup(StrArrayLoopSubGroup)
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
Next
Else
End If
If the group is not in the cache, we add it by calling RecursiveGroupSub
. This will also put all sub-groups and AD users in the cache. The RecursiveGroupSub
sub has logic to break circular dependencies. This can happen if you have two groups that have each other as members. Lastly, just write out any unresolved records; this could be an account or group from a trusted domain.
Else
If RecursiveGroupSub(objMember) = arrDUSplit(1) Then
If Not dicADGroup.ContainsKey(arrDUSplit(1)) Then GoTo ErrorHandle
Else
If Not dicADGroup.ContainsKey(RecursiveGroupSub(objMember)) Then GoTo ErrorHandle
End If
If dicADGroupManager.ContainsKey(arrDUSplit(1)) Then strManagedBy = dicADGroupManager(objMember.Properties("sAMAccountName").Value.ToString)
For Each StrArrayLoopUser In dicADGroup(arrDUSplit(1))
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
If dicADGroupSubGroup.ContainsKey(arrDUSplit(1)) Then
For Each StrArrayLoopSubGroup In dicADGroupSubGroup(arrDUSplit(1))
strManagedBy = ""
If dicADGroupManager.ContainsKey(StrArrayLoopSubGroup) Then strManagedBy = dicADGroupManager(StrArrayLoopSubGroup)
For Each StrArrayLoopUser In dicADGroup(StrArrayLoopSubGroup)
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
Next
Else
End If
End If
Case "contact"
Case Else
MsgBox("Unexpected strClass: " & strClass)
End Select
Case Else
strSAMA = arrDUSplit(1)
strGroupSAMA = arrDUSplit(0)
strManagedBy = arrDUSplit(1) & " Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8};{9}", Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, strDate, Environment.MachineName.ToString, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
End Select
Ending of the sub with the error handler.
End If
Next
Exit Sub
ErrorHandle:
Select Case Err.Number
Case 0
strTemp = "Error Number:" & Err.Number & " " & Err.Description & vbCrLf _
& " Sub ReadACLs: " & vbCrLf _
& vbTab & "DirInfo.FullName: " & strInput & vbCrLf _
& vbTab & "NTFS ACL:" & StrACLIdentityReference & vbCrLf _
& vbTab & "User:" & strSAMA & vbCrLf _
& vbTab & "arrDUSplit(0): " & arrDUSplit(0) & vbCrLf _
& vbTab & "arrDUSplit(1): " & arrDUSplit(1) & vbCrLf
CurrentRecord_Event(strTemp)
Threading.Thread.Sleep(0)
Err.Clear()
Case 5
strTemp = "Error Number:" & Err.Number & " " & Err.Description & vbCrLf _
& " Sub ReadACLs: " & vbCrLf _
& vbTab & "DirInfo.FullName: " & strInput & vbCrLf _
& vbTab & "NTFS ACL:" & StrACLIdentityReference & vbCrLf _
& vbTab & "User:" & strSAMA & vbCrLf _
& vbTab & "arrDUSplit(0): " & arrDUSplit(0) & vbCrLf _
& vbTab & "arrDUSplit(1): " & arrDUSplit(1) & vbCrLf
CurrentRecord_Event(strTemp)
Threading.Thread.Sleep(0)
Err.Clear()
Resume Next
Case 91
strTemp = "Error Number:" & Err.Number & " " & Err.Description & vbCrLf _
& " Sub ReadACLs: " & vbCrLf _
& vbTab & "DirInfo.FullName: " & strInput & vbCrLf _
& vbTab & "NTFS ACL:" & StrACLIdentityReference & vbCrLf _
& vbTab & "User:" & strSAMA & vbCrLf _
& vbTab & "arrDUSplit(0): " & arrDUSplit(0) & vbCrLf _
& vbTab & "arrDUSplit(1): " & arrDUSplit(1) & vbCrLf
CurrentRecord_Event(strTemp)
Threading.Thread.Sleep(0)
Err.Clear()
Resume Next
Case Else
strTemp = "Error Number:" & Err.Number & " " & Err.Description & vbCrLf _
& " Sub ReadACLs: " & vbCrLf _
& vbTab & "DirInfo.FullName: " & strInput & vbCrLf _
& vbTab & "NTFS ACL:" & StrACLIdentityReference & vbCrLf _
& vbTab & "User:" & strSAMA & vbCrLf _
& vbTab & "arrDUSplit(0): " & arrDUSplit(0) & vbCrLf _
& vbTab & "arrDUSplit(1): " & arrDUSplit(1) & vbCrLf
CurrentRecord_Event(strTemp)
Threading.Thread.Sleep(0)
If MsgBox("Error Number:" & Err.Number & " " & Err.Description & vbCrLf _
& " Sub ReadACLs: " & vbCrLf _
& vbTab & "Input Directory: " & strInput & vbCrLf _
& vbTab & "NTFS ACL:" & StrACLIdentityReference & vbCrLf _
& vbTab & "User:" & strSAMA & vbCrLf _
& vbTab & "arrDUSplit(0): " & arrDUSplit(0) & vbCrLf _
& vbTab & "arrDUSplit(1): " & arrDUSplit(1) & vbCrLf _
, MsgBoxStyle.Critical, "Critical Error File Server Audit 2 Quitting") Then
Err.Clear()
Main.Close()
Else
Err.Clear()
Exit Sub
End If
End Select
End Sub
Points of Interest
There are always bugs in code and input and you can never code around them all; but you can get it to work for what you want to do. Thanks for everyone that helped me learn.
History
- Version 2.2.1 (2015-06-24)
- Added Computer name field for database. This allows for better reporting
- Updated MySQL drivers
- Added SQL Caching so 500 records are batched and ented at one time reducing load on SQL server and local machine.
- Other bug fixes
- Updated to use .Net 4.5 (Needed for MySQL drivers)
MSSQL Table Name: FileAudit
Column Name | Data Type |
ID | int |
FolderPath | nvarchar(MAX) |
AccountSAMAccountName | nvarchar(MAX) |
GroupSAMAccountName | nvarchar(MAX) |
ManagedBy | nvarchar(MAX) |
Inheritance | nvarchar(MAX) |
IsInherited | nvarchar(MAX) |
Rights | nvarchar(MAX) |
Owner | nvarchar(MAX) |
Computer | nvarchar(MAX) |
RunDate | bigint |
MySQL Table Name: FileAudit
Column Name | Data Type |
ID | int |
FolderPath | LONGTEXT |
AccountSAMAccountName | LONGTEXT |
GroupSAMAccountName | LONGTEXT |
ManagedBy | LONGTEXT |
Inheritance | LONGTEXT |
IsInherited | LONGTEXT |
Rights | LONGTEXT |
Owner | LONGTEXT |
Computer | LONGTEXT |
RunDate | bigint |
- Version 2.2.0 (2011-10-28)
- Removed hardcoded database names.
- Added support for MySQL.
- Removed all log file output.
- Cleaned up interface.
- Version 2.1.1 (2011-05-02)
- Fixed error: Circular group dependency message.
- Changed grouping so less accounts are marked as Direct and system accounts are marked as System.
- Version 2.1.0
dicADUserType | 'Hold cache for AD Users/Groups (AD sAMAccountName, AD Object Type) |
dicADGroup | 'Holds group sAMAccountName and array of users sAMAccountName |
dicADGroupManager | 'Holds group sAMAccountName and managed by name |
dicADGroupSubGroup | 'Holds group sAMAccountName and sub-group sAMAccountName |
- Added checks for circular dependency. This avoids infinite loops; also logs as an error.
- Lumped Computer with
User
class. This means that computers ACLs are treated as User ACLs. - Added Exception for
Contact
class. - Switched out arrays for
Dictionary
s. This makes caching easier to understand and less looping.
- Version 2.0.9
- Fixed issue with deleted accounts.
- Fixed issue with AD caching.
- Fixed issue with Start button.
- Added a Delete all SQL records.
- Version 2.0.8
- Version 2.0.7
- Fixed issue where program would skip over folder if there was only one sub-folder.
- Changed it so that all errors would be printed in the Output tab.
- Fixed issue where inserting in to SQL Server could cause array out of bounds.
- Fixed issue about reporting errors to GUI.
- Changed update directory to \\ucpg-files.uchicago.edu\Installs\CustomTools\File Server Audit.
- Added a feature to remove old SQL records from the current day.
- Trying to fix Cancel button.
- Version 2.0.6
- Added Total Execution Time to notify on end.
- Version 2.0.5
- Added field
IsInherited
to text output. - Added table field:
IsInherited varchar(50)
.
- Version 2.0.4
Table name: FileAudit
Column Name | Data Type |
ID | int |
FolderPath | nvarchar(MAX) |
AccountSAMAccountName | nvarchar(MAX) |
GroupSAMAccountName | nvarchar(MAX) |
ManagedBy | nvarchar(MAX) |
Inheritance | nvarchar(MAX) |
Rights | nvarchar(MAX) |
Owner | nvarchar(MAX) |
RunDate | bigint |
- Fixed an array issue where zero sub-directories created an error.
- Unknown Domains get no group enumeration.
- Added Managed By for group membership.
- Added inserting data directly into SQL database.
- Version 2.0.3
- Added Remove Inherited feature that only shows permissions that are not inherited.
- Version 2.0.2
- Commented most of the code.
- Updated separate file log classes.
- Version 2.0.1
- Added a cache for AD groups to speed up enumeration.
- Added a cache for AD users to speed up enumeration.
- Fixed logic problem with
RecursiveSearch
sub.