Find out username(who) modified file in C#

I am using a FileSystemWatcher to monitor a folder. But when there is some event happening in the directory, I don't know how to search who made a impact on that file. I tried to use EventLog. It just couldn't work. Is there another way to do it?


Solution 1:

I cant remember where I found this code but its an alternative to using pInvoke which I think is a bit overkill for this task. Use the FileSystemWatcher to watch the folder and when an event fires you can work out which user made the file change using this code:

private string GetSpecificFileProperties(string file, params int[] indexes)
{
    string fileName = Path.GetFileName(file);
    string folderName = Path.GetDirectoryName(file);
    Shell32.Shell shell = new Shell32.Shell();
    Shell32.Folder objFolder;
    objFolder = shell.NameSpace(folderName);
    StringBuilder sb = new StringBuilder();

    foreach (Shell32.FolderItem2 item in objFolder.Items())
    {
        if (fileName == item.Name)
        {
            for (int i = 0; i < indexes.Length; i++)
            {
                sb.Append(objFolder.GetDetailsOf(item, indexes[i]) + ",");
            }

            break;
        }
    }

    string result = sb.ToString().Trim();
    //Protection for no results causing an exception on the `SubString` method
    if (result.Length == 0)
    {
        return string.Empty;
    }
    return result.Substring(0, result.Length - 1);
}

Shell32 is a reference to the DLL: Microsoft Shell Controls And Automation - its a COM reference

Here is some example's of how you call the method:

string Type = GetSpecificFileProperties(filePath, 2);
string ObjectKind = GetSpecificFileProperties(filePath, 11);
DateTime CreatedDate = Convert.ToDateTime(GetSpecificFileProperties(filePath, 4));
DateTime LastModifiedDate = Convert.ToDateTime(GetSpecificFileProperties(filePath, 3));
DateTime LastAccessDate = Convert.ToDateTime(GetSpecificFileProperties(filePath, 5));
string LastUser = GetSpecificFileProperties(filePath, 10);
string ComputerName = GetSpecificFileProperties(filePath, 53);
string FileSize = GetSpecificFileProperties(filePath, 1);

Or get multiple comma separated properties together:

string SizeTypeAndLastModDate = GetSpecificFileProperties(filePath, new int[] {1, 2, 3});

Note: This solution has been tested on Windows 7 and Windows 10. It wont work unless running in a STA as per Exception when using Shell32 to get File extended properties and you will see the following error:

Unable to cast COM object of type 'Shell32.ShellClass' to interface type 'Shell32.IShellDispatch6'

Solution 2:

You need to enable auditing on the file system (and auditing is only available on NTFS). You do this by applying a group policy or local security policy. You will also have to enable auditing on the file you want to monitor. You do it the same way as you modify the permissions on the file.

Auditing events are then written to the security event log. You will have to monitor this event log for the auditing events you are interested in. One way to do this is to create a scheduled task that starts an application when the events you are interested in are logged. Starting a new process for each event is only viable if events aren't logged at a very high rate though. Otherwise you will likely experience performance problems.

Basically, you don't want to look at the contents or attributes of the file (which the shell function GetFileDetails does). Also, you don't want to use a file sharing API to get the network user that has the file open (which NetGetFileInfo does). You want to know the user of the process that last modified the file. This information is not normally recorded by Windows because it would require too many resources to do that for all file activities. Instead you can selectively enable auditing for specific users doing specifc actions on specific files (and folders).

Solution 3:

It seems that you'll need to invoke Windows API functions to get what you want, which involves PInvoke. Some people on another forum have been looking into it and figured something out, you can find their solution here. However, it seems to work only with files on network shares (not on your local machine).

For future reference, this is the code posted by dave4dl:

[DllImport("Netapi32.dll", SetLastError = true)]
static extern int NetApiBufferFree(IntPtr Buffer);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
struct FILE_INFO_3
{
    public int fi3_id;
    public int fi3_permission;
    public int fi3_num_locks;
    public string fi3_pathname;
    public string fi3_username;
}

[DllImport("netapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern int NetFileEnum(
     string servername,
     string basepath,
     string username,
     int level,
     ref IntPtr bufptr,
     int prefmaxlen,
     out int entriesread,
     out int totalentries,
     IntPtr resume_handle
);

[DllImport("netapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern int NetFileGetInfo(
  string servername,
  int fileid,
  int level,
  ref IntPtr bufptr
);

private int GetFileIdFromPath(string filePath)
{
    const int MAX_PREFERRED_LENGTH = -1;

    int dwReadEntries;
    int dwTotalEntries;
    IntPtr pBuffer = IntPtr.Zero;
    FILE_INFO_3 pCurrent = new FILE_INFO_3();

    int dwStatus = NetFileEnum(null, filePath, null, 3, ref pBuffer, MAX_PREFERRED_LENGTH, out dwReadEntries, out dwTotalEntries, IntPtr.Zero);

    if (dwStatus == 0)
    {
        for (int dwIndex = 0; dwIndex < dwReadEntries; dwIndex++)
        {

            IntPtr iPtr = new IntPtr(pBuffer.ToInt32() + (dwIndex * Marshal.SizeOf(pCurrent)));
            pCurrent = (FILE_INFO_3)Marshal.PtrToStructure(iPtr, typeof(FILE_INFO_3));

            int fileId = pCurrent.fi3_id;

            //because of the path filter in the NetFileEnum function call, the first (and hopefully only) entry should be the correct one
            NetApiBufferFree(pBuffer);
            return fileId;
        }
    }

    NetApiBufferFree(pBuffer);
    return -1;  //should probably do something else here like throw an error
}


private string GetUsernameHandlingFile(int fileId)
{
    string defaultValue = "[Unknown User]";

    if (fileId == -1)
    {
        return defaultValue;
    }

    IntPtr pBuffer_Info = IntPtr.Zero;
    int dwStatus_Info = NetFileGetInfo(null, fileId, 3, ref pBuffer_Info);

    if (dwStatus_Info == 0)
    {
        IntPtr iPtr_Info = new IntPtr(pBuffer_Info.ToInt32());
        FILE_INFO_3 pCurrent_Info = (FILE_INFO_3)Marshal.PtrToStructure(iPtr_Info, typeof(FILE_INFO_3));
        NetApiBufferFree(pBuffer_Info);
        return pCurrent_Info.fi3_username;
    }

    NetApiBufferFree(pBuffer_Info);
    return defaultValue;  //default if not successfull above
}

private string GetUsernameHandlingFile(string filePath)
{
    int fileId = GetFileIdFromPath(filePath);
    return GetUsernameHandlingFile(fileId);
}