File Server Audit
Introduction
File Server Audit solves the problem of getting the 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 use full reports to find what a user has access too. 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.
Background
I wrote this part of a File server migration and cleanup project; to tell me all who had access to files and folders. This program was really helpful 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 need 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 and is hard to sort and analyze.
Global Variables; Uses Dictionaries as local cache to speed up AD records.
Dim blnShowOutput As Boolean = True 'Used to show output
Dim blnInherited As Boolean = False 'Used to remove inherited ACLs
Dim strDate As String = TodayFileDate() 'Holds Date in string format
Dim dicADUserType As New Dictionary(Of String, String) 'Hold Cache for _
'AD Users/Groups (AD sAMAccountName, AD Object Type)
Dim dicADGroup As New Dictionary(Of String, Array) 'Holds Group sAMAccountName
'and Array of Users sAMAccountName
Dim dicADGroupManager As New Dictionary(Of String, String) 'Holds Group sAMAccountName
'and Managed By name
Dim dicADGroupSubGroup As New Dictionary(Of String, Array) 'Holds Group
'sAMAccountName and Sub-Group sAMAccountName
Setup for the ReadACLs sub
that reads the actual ACLs. In the setup, DirInfo
binds to the input directory to 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)
'strInput is the full path to a folder
'blnRemoveInherited is used to remove inherited ACLs to reduce output
'Bindings:
'Binds to the Directory using the string provided
Dim DirInfo As System.IO.DirectoryInfo = _
New System.IO.DirectoryInfo(strInput) 'Directory is already binded
'Binds to Directory Security
'This allows user to read ACLs
Dim DirSec As System.Security.AccessControl.DirectorySecurity = _
DirInfo.GetAccessControl()
'Gets Directory Access Control Lists
'Collection is returned with ACLs so we can enumerate later
Dim ACLs As System.Security.AccessControl.AuthorizationRuleCollection = _
DirSec.GetAccessRules(True, True, GetType(System.Security.Principal.NTAccount))
'Access Control List Entry
'Variable to hold a ACL
Dim ACL As System.Security.AccessControl.FileSystemAccessRule
'Gets Folder owner ID
Dim Owner As System.Security.Principal.IdentityReference = _
DirSec.GetOwner(GetType(System.Security.Principal.NTAccount))
'Binds to the default domain
'For this to work this will have to run under a domain account
Dim DomainContext As New _
System.DirectoryServices.ActiveDirectory.DirectoryContext_
(System.DirectoryServices.ActiveDirectory.DirectoryContextType.Domain)
'Binds to AD to get account and group info
Dim objMember As New System.DirectoryServices.DirectoryEntry
'Declarations:
Dim arrDUSplit(1) As String 'Used to hold domain and user ex domain\user
Dim strTemp As String = "" 'Holds working String
Dim strSAMA As String = "" 'Users Username ("sAMAccountName")
Dim strClass As String = "" ' AD Object type Group or User
Dim strGroupSAMA As String = "" 'The Group Users is in
Dim strManagedBy As String = "" 'Person that manages that AD Group.
Dim strOwner As String = Owner.ToString 'Holds Owners name as string.
'Converts folders Owners Object to String
Dim StrACLIdentityReference As String = "" 'Holds ACL.IdentityReference.ToString
'Properties
Dim StrInheritanceFlags As String = "" 'Holds ACL.InheritanceFlags.ToString
'Properties
Dim StrFileSystemRights As String = "" 'Holds ACL.FileSystemRights.ToString
'Properties
Dim StrIsInherited As String = "" 'Holds ACL.IsInherited.ToString Properties
Dim StrArrayLoopSubGroup As String 'Allows Looping thru all sub-group
Dim StrArrayLoopUser As String 'Allows Looping thru all Users
On Error GoTo ErrorHandle
'Main Section of sub
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 too. 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.
'Loops through all the Access Control Lists For the Folder
For Each ACL In ACLs
'Setup Most Used ACL Properties
StrACLIdentityReference = ACL.IdentityReference.ToString
StrInheritanceFlags = ACL.InheritanceFlags.ToString
StrFileSystemRights = ACL.FileSystemRights.ToString
StrIsInherited = ACL.IsInherited.ToString
If Str.Left(StrACLIdentityReference, 2) = "S-" Then
'Used to catch Deleted Accounts.
strSAMA = "Unknown"
strGroupSAMA = StrACLIdentityReference
strManagedBy = "Unknown"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8}", _
Str.Replace(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, _
strOwner, strDate, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len(strInput) >= 3 _
Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Else
'Splits Domain and User or Group a part.
'This cleans up the output a little\.
arrDUSplit = Split(StrACLIdentityReference, "\")
'Formats the output differently depending on the domain.
Select Case arrDUSplit(0)
'Built-in security principals need special treatment
Case "BUILTIN"
strSAMA = arrDUSplit(1)
strGroupSAMA = "System"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};_
{6};{7};{8}", Str.Replace(DirInfo.FullName, _
";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, _
strDate, 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}", Str.Replace_
(DirInfo.FullName, ";", ","), strSAMA, _
strGroupSAMA, strManagedBy, _
StrInheritanceFlags, StrFileSystemRights, _
strOwner, strDate, 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}", Str.Replace(DirInfo.FullName, _
";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, _
strDate, 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}", Str.Replace_
(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, strDate, 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
'Used for Local Users that have the
'Domain of the computer name.
strSAMA = arrDUSplit(1)
strGroupSAMA = "Local"
strManagedBy = "Systems Administrators"
strTemp = String.Format("{0};{1};{2};{3};_
{4};{5};{6};{7};{8}", Str.Replace_
(DirInfo.FullName, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, _
strDate, 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 of 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 the RecursiveGroupSub
to get all members of that group and any sub-groups; then we add those to the cache.
Case Environment.UserDomainName.ToString
'Uses Currently login domain to enumerate groups
'Checks to see if the account type is in the Cache
If dicADUserType.ContainsKey(arrDUSplit(1)) Then
strSAMA = arrDUSplit(1)
strClass = dicADUserType(arrDUSplit(1)).ToString
Else
'If AD Users are not in the Cache Dictionary Check AD and
'Put them in the Cache
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 'Get Record
'AD Type User or Group
strSAMA = objMember.Properties_
("sAMAccountName").Value.ToString 'Get AD UserName
dicADUserType.Add(strSAMA, strClass) 'sets username
'in Dictionary,
'sets class in Dictionary
End If
End If
'Do different actions based on AD Class
Select Case strClass
'Write out Each User
'Each of this Users has explicit permissions
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}", Str.Replace(DirInfo.FullName, ";", ","), _
strSAMA, strGroupSAMA, strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, strDate, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or Str.Len_
(strInput) >= 3 Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Case "group"
'For Groups do a recursive search for all sub groups and users
'Check to see of Group is in Dictionary.
If dicADGroup.ContainsKey(arrDUSplit(1)) Then
'Set Manager Of Group
strManagedBy = ""
If dicADGroupManager.ContainsKey(arrDUSplit(1)) _
Then strManagedBy = dicADGroupManager(arrDUSplit(1))
'Loop thru all users in that group.
For Each StrArrayLoopUser In _
dicADGroup(arrDUSplit(1)) 'Loops thru all user _
in a group
'AD Group was Found in Cache.
strTemp = String.Format("{0};{1};{2};_
{3};{4};{5};{6};{7};{8}", Str.Replace_
(DirInfo.FullName, ";", ","), _
StrArrayLoopUser, arrDUSplit(1), _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, _
strOwner, strDate, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or _
Str.Len(strInput) >= 3 _
Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
'Loop thru all sub Groups
If dicADGroupSubGroup.ContainsKey(arrDUSplit(1)) Then
For Each StrArrayLoopSubGroup _
In dicADGroupSubGroup(arrDUSplit(1)) 'Loops
'through all sub-group
strManagedBy = ""
If dicADGroupManager.ContainsKey_
(StrArrayLoopSubGroup) Then _
strManagedBy = dicADGroupManager_
(arrDUSplit(1))
'Test SubGroup to see if they
'are in cache
If Not dicADGroup.ContainsKey_
(StrArrayLoopSubGroup) Then
If Not RecursiveGroupSub_
(objMember) = _
StrArrayLoopSubGroup _
Then GoTo ErrorHandle
End If
For Each StrArrayLoopUser In _
dicADGroup(StrArrayLoopSubGroup)'Loops
' thru all user in a group
'AD Group was Found in Cache.
strTemp = String.Format_
("{0};{1};{2};{3};{4};{5};_
{6};{7};{8}", Str.Replace_
(DirInfo.FullName, _
";", ","), _
StrArrayLoopUser, _
StrArrayLoopSubGroup, _
strManagedBy, _
StrInheritanceFlags, _
StrFileSystemRights, _
strOwner, strDate, _
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
Else
If the group is not in the cache, we add it here by calling RecursiveGroupSub
. This will also put all sub-groups and AD users in the cache too. 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.
'Group is not is Cache so start
'RecursiveGroupSub to put it in Cache
'RecursiveGroupSub should return the group
'it is worked on
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
'Set the Manager of the group
If dicADGroupManager.ContainsKey(arrDUSplit(1)) Then
strManagedBy = dicADGroupManager_
(objMember.Properties_
("sAMAccountName").Value.ToString)
End If
'Loop thru all user on the Group
For Each StrArrayLoopUser In dicADGroup_
(arrDUSplit(1)) 'Loops thru all user in a group
'AD Group was Found in Cache.
strTemp = String.Format("{0};{1};{2};_
{3};{4};{5};{6};{7};{8}", _
Str.Replace(DirInfo.FullName, ";", ","), _
StrArrayLoopUser, arrDUSplit(1), _
strManagedBy, StrInheritanceFlags, _
StrFileSystemRights, strOwner, _
strDate, StrIsInherited)
If blnRemoveInherited Then
If ACL.IsInherited = False Or _
Str.Len(strInput) >= 3 _
Then LogWrite(strTemp)
Else
LogWrite(strTemp)
End If
Next
'Loop thru all sub Groups
If dicADGroupSubGroup.ContainsKey(arrDUSplit(1)) Then
For Each StrArrayLoopSubGroup In _
dicADGroupSubGroup(arrDUSplit(1)) 'Loops
'thru all sub-group
strManagedBy = ""
If dicADGroupManager.ContainsKey_
(StrArrayLoopSubGroup) Then
strManagedBy = _
dicADGroupManager_
(StrArrayLoopSubGroup)
End If
For Each StrArrayLoopUser _
In dicADGroup
(StrArrayLoopSubGroup) 'Loops
'thru all user in a group
'AD Group was Found in Cache.
strTemp = String.Format("{0};_
{1};{2};{3};{4};{5};{6};_
{7};{8}", Str.Replace_
(DirInfo.FullName, _
";", ","), _
StrArrayLoopUser, _
StrArrayLoopSubGroup, _
strManagedBy, _
StrInheritanceFlags, _
StrFileSystemRights, _
strOwner, strDate, 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
'Catch all just Write out Entries without group enumeration
'If you have multiple domains in your forest the domain you are
'not logon on too will show up here.
strSAMA = arrDUSplit(1)
strGroupSAMA = arrDUSplit(0)
strManagedBy = arrDUSplit(1) & " Administrators"
strTemp = String.Format("{0};{1};{2};{3};{4};{5};{6};{7};{8}", _
Str.Replace(strInput, ";", ","), strSAMA, strGroupSAMA, _
strManagedBy, StrInheritanceFlags, StrFileSystemRights, strOwner, _
strDate, 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 'The given key was not present in the dictionary.
'Log error in the GUI
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
'Log error in the GUI
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)
'Message Box about the error click ok to quit or X to continue
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()
Form1.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 to everyone that helped me learn.
History
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
- 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
forContact
class. - Switch out Arrays for
Dictionary
. This makes caching easier to understand and less looping.
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 |
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
- Updated SQL Server
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 into SQL 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
- 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 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
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 Logs 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