using Alphaleonis.Win32.Filesystem; using System; using System.Collections.Generic; using System.Security.AccessControl; namespace Security2 { public class FileSystemSecurity2 { protected FileSystemInfo item; protected FileSystemSecurity sd; protected AccessControlSections sections; protected bool isFile = false; // The SDDL form of each section as it was read or last written, so that WriteChanges writes only the // sections that changed since. private Dictionary sectionsAsRead; public FileSystemInfo Item { get { return item; } set { item = value; } } public string FullName { get { return item.FullName; } } public string Name { get { return item.Name; } } public bool IsFile { get { return isFile; } } public FileSystemSecurity2(FileSystemInfo item, AccessControlSections sections) { this.sections = sections; this.item = item; isFile = item is FileInfo; sd = GetSecurity(item, sections); RememberSections(); } public FileSystemSecurity2(FileSystemInfo item) { this.item = item; isFile = item is FileInfo; try { sd = GetSecurity(item, AccessControlSections.All); sections = AccessControlSections.All; } catch { try { sd = GetSecurity(item, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); sections = AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group; } catch { sd = GetSecurity(item, AccessControlSections.Access); sections = AccessControlSections.Access; } } // Read together with the SACL, the inherited entries of a DACL without the auto-inherit flag lose their // inherited flag when the parent folder has no SACL, and writing such a DACL back stores them as explicit // entries. Read alone, the DACL keeps the flags. // // The same goes for the SACL: read together with the other sections, the SACL of an item without audit // entries is reported as protected from inheritance on some computers (seen on domain-joined Windows Server // 2022 and 2025 and on Windows 11), while the read of the SACL alone, which Get-NTFSInheritance uses for a // path, reports it as not protected. The state of the item has to be the same by path and by descriptor. if (HasAuditSection) { var accessSecurity = GetSecurity(item, AccessControlSections.Access); sd.SetSecurityDescriptorBinaryForm(accessSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Access); var auditSecurity = GetSecurity(item, AccessControlSections.Audit); sd.SetSecurityDescriptorBinaryForm(auditSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Audit); } RememberSections(); } // For the root of a drive, the methods of DirectoryInfo read and write the security descriptor of the volume, // a device object, instead of that of its root folder (#41). The methods that take the path keep the trailing // backslash and reach the root folder. internal static FileSystemSecurity GetSecurity(FileSystemInfo item, AccessControlSections sections) { var file = item as FileInfo; if (file != null) { return file.GetAccessControl(sections); } string root; if (TryGetDriveRoot(item, out root)) { return Directory.GetAccessControl(root, sections); } return ((DirectoryInfo)item).GetAccessControl(sections); } internal static void SetSecurity(FileSystemInfo item, FileSystemSecurity security, AccessControlSections sections) { var file = item as FileInfo; if (file != null) { file.SetAccessControl((FileSecurity)security, sections); return; } string root; if (TryGetDriveRoot(item, out root)) { Directory.SetAccessControl(root, (DirectorySecurity)security, sections); return; } ((DirectoryInfo)item).SetAccessControl((DirectorySecurity)security, sections); } private static bool TryGetDriveRoot(FileSystemInfo item, out string root) { var fullName = item.FullName.TrimEnd('\\'); // A drive letter, such as C: var isDriveLetter = fullName.Length == 2 && fullName[1] == ':' && ((fullName[0] >= 'A' && fullName[0] <= 'Z') || (fullName[0] >= 'a' && fullName[0] <= 'z')); // A volume name, such as \\?\Volume{9f122b1b-858a-49bd-aec4-dfbe8978fe16}, without a folder below it var isVolumeName = fullName.StartsWith(@"\\?\Volume{", StringComparison.OrdinalIgnoreCase) && fullName.EndsWith("}", StringComparison.Ordinal) && fullName.IndexOf('\\', 4) < 0; if (isDriveLetter || isVolumeName) { root = fullName + "\\"; return true; } root = null; return false; } // Without the Security privilege, the security descriptor is read without its SACL. internal bool HasAuditSection { get { return (sections & AccessControlSections.Audit) == AccessControlSections.Audit; } } // An item without audit entries can have no SACL at all, also when the SACL was read. internal bool HasSystemAcl { get { return new RawSecurityDescriptor(sd.GetSecurityDescriptorBinaryForm(), 0).SystemAcl != null; } } // The sections that differ from the ones that were read or last written. internal AccessControlSections ChangedSections { get { var changed = AccessControlSections.None; foreach (var section in sectionsAsRead) { if (sd.GetSecurityDescriptorSddlForm(section.Key) != section.Value) { changed |= section.Key; } } return changed; } } private void RememberSections() { sectionsAsRead = new Dictionary(); foreach (var section in new[] { AccessControlSections.Access, AccessControlSections.Audit, AccessControlSections.Owner, AccessControlSections.Group }) { sectionsAsRead[section] = sd.GetSecurityDescriptorSddlForm(section); } } public FileSystemSecurity SecurityDescriptor { get { return sd; } } // Writes the sections that the descriptor was read with. Windows can return the owner and the group with a DACL // that is read alone, and writing them back fails for an owner that the user cannot assign (#34). public void Write() { SetSecurity(item, sd, sections); RememberSections(); } // Writes only the sections that changed since they were read or last written, so that, for example, an // unchanged owner that the user cannot assign isn't written back (#34). Without a change, it writes nothing. internal void WriteChanges() { var changedSections = ChangedSections; if (changedSections == AccessControlSections.None) { return; } SetSecurity(item, sd, changedSections); RememberSections(); } // Writes the sections that the descriptor was read with to another item, like Write(). public void Write(FileSystemInfo item) { SetSecurity(item, sd, sections); } public void Write(string path) { FileSystemInfo item = null; if (File.Exists(path)) { item = new FileInfo(path); } else if (Directory.Exists(path)) { item = new DirectoryInfo(path); } else { throw new System.IO.FileNotFoundException("File not found", path); } Write(item); } #region Conversion // The descriptor is in sd. Before 5.0.0-rc6, these conversions returned fields that were never set, so they // gave null, and the hash code threw a NullReferenceException. public static implicit operator FileSecurity(FileSystemSecurity2 fs2) { return fs2.sd as FileSecurity; } public static implicit operator FileSystemSecurity2(FileSecurity fs) { return new FileSystemSecurity2(new FileInfo("")); } public static implicit operator DirectorySecurity(FileSystemSecurity2 fs2) { return fs2.sd as DirectorySecurity; } public static implicit operator FileSystemSecurity2(DirectorySecurity fs) { return new FileSystemSecurity2(new DirectoryInfo("")); } // Like the descriptors of .NET, two objects are equal when they wrap the same descriptor; a descriptor of .NET // is never equal, so that equality is the same in both directions. A cast instead of "as" threw an // InvalidCastException. public override bool Equals(object obj) { var other = obj as FileSystemSecurity2; return other != null && ReferenceEquals(sd, other.sd); } public override int GetHashCode() { return sd != null ? sd.GetHashCode() : 0; } #endregion public static void ConvertToFileSystemFlags(ApplyTo ApplyTo, out InheritanceFlags inheritanceFlags, out PropagationFlags propagationFlags) { inheritanceFlags = InheritanceFlags.None; propagationFlags = PropagationFlags.None; switch (ApplyTo) { case ApplyTo.FilesOnly: inheritanceFlags = InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.InheritOnly; break; case ApplyTo.SubfoldersAndFilesOnly: inheritanceFlags = InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit; propagationFlags = PropagationFlags.InheritOnly; break; case ApplyTo.SubfoldersOnly: inheritanceFlags = InheritanceFlags.ContainerInherit; propagationFlags = PropagationFlags.InheritOnly; break; case ApplyTo.ThisFolderAndFiles: inheritanceFlags = InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.None; break; case ApplyTo.ThisFolderAndSubfolders: inheritanceFlags = InheritanceFlags.ContainerInherit; propagationFlags = PropagationFlags.None; break; case ApplyTo.ThisFolderOnly: inheritanceFlags = InheritanceFlags.None; propagationFlags = PropagationFlags.None; break; case ApplyTo.ThisFolderSubfoldersAndFiles: inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.None; break; case ApplyTo.FilesOnlyOneLevel: inheritanceFlags = InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit; break; case ApplyTo.SubfoldersAndFilesOnlyOneLevel: inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit; break; case ApplyTo.SubfoldersOnlyOneLevel: inheritanceFlags = InheritanceFlags.ContainerInherit; propagationFlags = PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit; break; case ApplyTo.ThisFolderAndFilesOneLevel: inheritanceFlags = InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.NoPropagateInherit; break; case ApplyTo.ThisFolderAndSubfoldersOneLevel: inheritanceFlags = InheritanceFlags.ContainerInherit; propagationFlags = PropagationFlags.NoPropagateInherit; break; case ApplyTo.ThisFolderSubfoldersAndFilesOneLevel: inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; propagationFlags = PropagationFlags.NoPropagateInherit; break; } } public static ApplyTo ConvertToApplyTo(InheritanceFlags InheritanceFlags, PropagationFlags PropagationFlags) { if (InheritanceFlags == InheritanceFlags.ObjectInherit & PropagationFlags == PropagationFlags.InheritOnly) return ApplyTo.FilesOnly; else if (InheritanceFlags == (InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit) & PropagationFlags == PropagationFlags.InheritOnly) return ApplyTo.SubfoldersAndFilesOnly; else if (InheritanceFlags == InheritanceFlags.ContainerInherit & PropagationFlags == PropagationFlags.InheritOnly) return ApplyTo.SubfoldersOnly; else if (InheritanceFlags == InheritanceFlags.ObjectInherit & PropagationFlags == PropagationFlags.None) return ApplyTo.ThisFolderAndFiles; else if (InheritanceFlags == InheritanceFlags.ContainerInherit & PropagationFlags == PropagationFlags.None) return ApplyTo.ThisFolderAndSubfolders; else if (InheritanceFlags == InheritanceFlags.None & PropagationFlags == PropagationFlags.None) return ApplyTo.ThisFolderOnly; else if (InheritanceFlags == (InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit) & PropagationFlags == PropagationFlags.None) return ApplyTo.ThisFolderSubfoldersAndFiles; else if (InheritanceFlags == (InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit) & PropagationFlags == PropagationFlags.NoPropagateInherit) return ApplyTo.ThisFolderSubfoldersAndFilesOneLevel; else if (InheritanceFlags == InheritanceFlags.ContainerInherit & PropagationFlags == PropagationFlags.NoPropagateInherit) return ApplyTo.ThisFolderAndSubfoldersOneLevel; else if (InheritanceFlags == InheritanceFlags.ObjectInherit & PropagationFlags == PropagationFlags.NoPropagateInherit) return ApplyTo.ThisFolderAndFilesOneLevel; else if (InheritanceFlags == (InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit) & PropagationFlags == (PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit)) return ApplyTo.SubfoldersAndFilesOnlyOneLevel; else if (InheritanceFlags == InheritanceFlags.ContainerInherit & PropagationFlags == (PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit)) return ApplyTo.SubfoldersOnlyOneLevel; else if (InheritanceFlags == InheritanceFlags.ObjectInherit & PropagationFlags == (PropagationFlags.InheritOnly | PropagationFlags.NoPropagateInherit)) return ApplyTo.FilesOnlyOneLevel; throw new RightsConverionException("The combination of InheritanceFlags and PropagationFlags could not be translated"); } public static FileSystemRights MapGenericRightsToFileSystemRights(uint originalRights) { try { var r = Enum.Parse(typeof(FileSystemRights), (originalRights).ToString()); if (r.ToString() == originalRights.ToString()) { throw new ArgumentOutOfRangeException(); } var fileSystemRights = (FileSystemRights)originalRights; return fileSystemRights; } catch (Exception) { FileSystemRights rights = 0; if (Convert.ToBoolean(originalRights & (uint)GenericRights.GENERIC_EXECUTE)) { rights |= (FileSystemRights)MappedGenericRights.FILE_GENERIC_EXECUTE; originalRights ^= (uint)GenericRights.GENERIC_EXECUTE; } if (Convert.ToBoolean(originalRights & (uint)GenericRights.GENERIC_READ)) { rights |= (FileSystemRights)MappedGenericRights.FILE_GENERIC_READ; originalRights ^= (uint)GenericRights.GENERIC_READ; } if (Convert.ToBoolean(originalRights & (uint)GenericRights.GENERIC_WRITE)) { rights |= (FileSystemRights)MappedGenericRights.FILE_GENERIC_WRITE; originalRights ^= (uint)GenericRights.GENERIC_WRITE; } if (Convert.ToBoolean(originalRights & (uint)GenericRights.GENERIC_ALL)) { rights |= (FileSystemRights)MappedGenericRights.FILE_GENERIC_ALL; originalRights ^= (uint)GenericRights.GENERIC_ALL; } //throw new RightsConverionException("Cannot convert GenericRights into FileSystemRights"); var remainingRights = (FileSystemRights)Enum.Parse(typeof(FileSystemRights), (originalRights).ToString()); rights |= remainingRights; return rights; } } } }