Browse Source

fix(access): report the folders that Get-NTFSSimpleAccess left out

Get-NTFSSimpleAccess compares each folder with its parent folder. A
folder whose parent folder it hadn't reported was left out, and with it
all of its subfolders; a drive root, which has no parent folder, was left
out or compared with the parent folder of the folder before it; and a
folder that came after its parent folder a second time failed with a
ReadError, "An item with the same key has already been added". Such
folders are now reported with all of their entries, and the paths are
compared without regard to case.

Decision 22, item 2: an assumption in autopilot, flagged for the
maintainer's review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/116/head
Raimund Andree 3 days ago
parent
commit
ea2f6dfa78
  1. 5
      CHANGELOG.md
  2. 4
      Docs/Cmdlets/Get-NTFSSimpleAccess.md
  3. 53
      NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
  4. 3
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  5. 35
      Tests/Access.Tests.ps1

5
CHANGELOG.md

@ -332,5 +332,10 @@ The format is based on
privilege, as for an item without orphaned entries, and wrote a warning
for an item that it couldn't read; it now writes a `ReadSecurityError`,
like `Get-NTFSAudit`
- Fix `Get-NTFSSimpleAccess`, which left out a folder whose parent folder it
hadn't reported, and with it all of its subfolders, and which compared a
drive root with the parent folder of the folder before it; such folders
are now reported with all of their entries. A folder that came after its
parent folder a second time failed with a `ReadError`
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

4
Docs/Cmdlets/Get-NTFSSimpleAccess.md

@ -29,7 +29,7 @@ Get-NTFSSimpleAccess [-IncludeRootFolder] [-SecurityDescriptor] <FileSystemSecur
Reads the access control entries of folders and writes them as `Security2.SimpleFileSystemAccessRule` objects whose rights are reduced to the three values `Read`, `Write`, and `Delete`. Reading rights such as `ReadData`, which on a folder is the right to list it (`ListDirectory`), `ReadAttributes`, or `Traverse` become `Read`, changing rights such as `CreateFiles`, `WriteAttributes`, `ChangePermissions`, or `TakeOwnership` become `Write`, and `Delete` and `DeleteSubdirectoriesAndFiles` become `Delete`; `FullControl` becomes all three. The result answers who may read, change, or delete in a folder without the detail of the full ACL.
The second simplification is that repetitions are left out. The first folder the cmdlet processes is reported with all of its entries, and for every folder that follows only the entries are reported that its parent folder does not already cover. An entry is covered when the parent has an entry for the same account and access type that includes at least the same simple rights. This makes a recursive listing show where permissions actually change instead of repeating the inherited ones on every level, and it requires the parent folder to be processed before its children, which `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` do by default.
The second simplification is that repetitions are left out. The first folder the cmdlet processes is reported with all of its entries, and so is every folder whose parent folder the cmdlet didn't report before, such as a drive root. For a folder whose parent folder it reported, only the entries are reported that the parent folder does not already cover. An entry is covered when the parent has an entry for the same account and access type that includes at least the same simple rights. This makes a recursive listing show where permissions actually change instead of repeating the inherited ones on every level, and it requires the parent folder to be processed before its children, which `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` do by default.
`-IncludeRootFolder` is on by default and adds the parent folder of the first path as the baseline for the comparison, which is why the first result usually belongs to the folder above the one that was asked for. Use `-IncludeRootFolder:$false` to start the comparison at the first path itself.
@ -192,6 +192,8 @@ The simplified rights hide which exact rights an account holds. Use `Get-NTFSAcc
Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view. It also showed no rights for an entry that grants only `ReadData`, which other tools than .NET create, and it left out the parent folder of a relative path with a single folder name, such as `Data`.
Before 5.0.0-rc7, the cmdlet left out a folder whose parent folder it hadn't reported, unless it was the first folder, and with it all of its subfolders. A drive root was left out as well, or compared with the parent folder of the folder before it, and a folder that came after its parent folder a second time failed with a `ReadError`.
## RELATED LINKS
[Get-NTFSAccess](Get-NTFSAccess.md)

53
NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs

@ -25,9 +25,9 @@ namespace NTFSSecurity
set { includeRootFolder = value; }
}
Dictionary<string, IEnumerable<SimpleFileSystemAccessRule>> previousAcls = new Dictionary<string, IEnumerable<SimpleFileSystemAccessRule>>();
// Case-insensitive, like the paths of Windows, which can reach the cmdlet in different cases.
Dictionary<string, IEnumerable<SimpleFileSystemAccessRule>> previousAcls = new Dictionary<string, IEnumerable<SimpleFileSystemAccessRule>>(StringComparer.OrdinalIgnoreCase);
DirectoryInfo item;
FileSystemInfo previousItem;
bool isFirstFolder = true;
protected override void ProcessRecord()
@ -73,16 +73,21 @@ namespace NTFSSecurity
var acl = FilterAccount(FileSystemAccessRule2.GetFileSystemAccessRules(item, !ExcludeExplicit, !ExcludeInherited).Select(ace => ace.ToSimpleFileSystemAccessRule2())).ToList();
FileSystemInfo parent = null;
try
{
previousItem = item.GetParent();
parent = item.GetParent();
}
catch { }
IEnumerable<SimpleFileSystemAccessRule> previousAcl = null;
if (isFirstFolder)
IEnumerable<SimpleFileSystemAccessRule> previousAcl;
// A folder whose parent folder the cmdlet didn't report, such as the first one or a drive root,
// is reported with all of its entries. Before 5.0.0-rc7, such a folder after the first one was
// left out, or a drive root was compared with the parent of the folder before it.
if (isFirstFolder || parent == null || !previousAcls.TryGetValue(parent.FullName, out previousAcl))
{
previousAcls.Add(item.FullName, acl);
previousAcls[item.FullName] = acl;
aceList.AddRange(acl);
acl.ForEach(ace => WriteObject(ace));
@ -90,32 +95,16 @@ namespace NTFSSecurity
}
else
{
if (previousAcls.ContainsKey(previousItem.FullName))
{
previousAcl = previousAcls[previousItem.FullName];
previousAcls.Add(item.FullName, acl);
List<SimpleFileSystemAccessRule> diffAcl = new List<SimpleFileSystemAccessRule>();
foreach (var ace in acl)
{
var equalsUser = previousAcl.Where(prevAce => prevAce.Identity == ace.Identity);
var equalsUserAndAccessType = previousAcl.Where(prevAce => prevAce.Identity == ace.Identity & prevAce.AccessControlType == ace.AccessControlType);
var equalsRights = previousAcl.Where(prevAce => (prevAce.AccessRights & ace.AccessRights) == ace.AccessRights);
var totalEqual = previousAcl.Where(prevAce => prevAce.Identity == ace.Identity & prevAce.AccessControlType == ace.AccessControlType & (prevAce.AccessRights & ace.AccessRights) == ace.AccessRights);
if (previousAcl.Where(prevAce =>
prevAce.AccessControlType == ace.AccessControlType &
(prevAce.AccessRights & ace.AccessRights) == ace.AccessRights &
prevAce.Identity == ace.Identity).Count() == 0)
{
diffAcl.Add(ace);
}
}
aceList.AddRange(diffAcl);
diffAcl.ForEach(ace => WriteObject(ace));
}
previousAcls[item.FullName] = acl;
// The entries that the parent folder doesn't cover for the same account and access type
var diffAcl = acl.Where(ace => !previousAcl.Any(prevAce =>
prevAce.AccessControlType == ace.AccessControlType &
(prevAce.AccessRights & ace.AccessRights) == ace.AccessRights &
prevAce.Identity == ace.Identity)).ToList();
aceList.AddRange(diffAcl);
diffAcl.ForEach(ace => WriteObject(ace));
}
}

3
NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml

@ -6386,7 +6386,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</command:details>
<maml:description>
<maml:para>Reads the access control entries of folders and writes them as `Security2.SimpleFileSystemAccessRule` objects whose rights are reduced to the three values `Read`, `Write`, and `Delete`. Reading rights such as `ReadData`, which on a folder is the right to list it (`ListDirectory`), `ReadAttributes`, or `Traverse` become `Read`, changing rights such as `CreateFiles`, `WriteAttributes`, `ChangePermissions`, or `TakeOwnership` become `Write`, and `Delete` and `DeleteSubdirectoriesAndFiles` become `Delete`; `FullControl` becomes all three. The result answers who may read, change, or delete in a folder without the detail of the full ACL.</maml:para>
<maml:para>The second simplification is that repetitions are left out. The first folder the cmdlet processes is reported with all of its entries, and for every folder that follows only the entries are reported that its parent folder does not already cover. An entry is covered when the parent has an entry for the same account and access type that includes at least the same simple rights. This makes a recursive listing show where permissions actually change instead of repeating the inherited ones on every level, and it requires the parent folder to be processed before its children, which `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` do by default.</maml:para>
<maml:para>The second simplification is that repetitions are left out. The first folder the cmdlet processes is reported with all of its entries, and so is every folder whose parent folder the cmdlet didn't report before, such as a drive root. For a folder whose parent folder it reported, only the entries are reported that the parent folder does not already cover. An entry is covered when the parent has an entry for the same account and access type that includes at least the same simple rights. This makes a recursive listing show where permissions actually change instead of repeating the inherited ones on every level, and it requires the parent folder to be processed before its children, which `Get-ChildItem`, `Get-ChildItem2`, and `Get-Item2` do by default.</maml:para>
<maml:para>`-IncludeRootFolder` is on by default and adds the parent folder of the first path as the baseline for the comparison, which is why the first result usually belongs to the folder above the one that was asked for. Use `-IncludeRootFolder:$false` to start the comparison at the first path itself.</maml:para>
<maml:para>The cmdlet only processes folders; a path that points to a file is skipped silently, while the security descriptor of a file is reported. Relative paths are resolved against the current location, and the current location is used when `-Path` is omitted. `-ExcludeInherited`, `-ExcludeExplicit`, and `-Account` work as in `Get-NTFSAccess`. With `-SecurityDescriptor`, the cmdlet reports the entries of a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without comparing them with a parent folder.</maml:para>
</maml:description>
@ -6629,6 +6629,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>When the module setting `EnablePrivileges` is `$true` (the default in the `PrivateData` section of NTFSSecurity.psd1), this cmdlet tries to enable the Backup, Restore, Take Ownership, and Security privileges while it runs and disables the privileges it enabled when it finishes. These privileges are only available in an elevated session of an account that holds them, such as a member of the local Administrators group. If a privilege cannot be enabled, the cmdlet continues without it and writes a debug message.</maml:para>
<maml:para>The simplified rights hide which exact rights an account holds. Use `Get-NTFSAccess` when you need the full access control entry, and `Get-NTFSEffectiveAccess` when you need the rights that result from all entries together.</maml:para>
<maml:para>Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor`, and its output had no table view. It also showed no rights for an entry that grants only `ReadData`, which other tools than .NET create, and it left out the parent folder of a relative path with a single folder name, such as `Data`.</maml:para>
<maml:para>Before 5.0.0-rc7, the cmdlet left out a folder whose parent folder it hadn't reported, unless it was the first folder, and with it all of its subfolders. A drive root was left out as well, or compared with the parent folder of the folder before it, and a folder that came after its parent folder a second time failed with a `ReadError`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>

35
Tests/Access.Tests.ps1

@ -326,6 +326,41 @@ Describe 'Get-NTFSSimpleAccess' {
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
}
# Before 5.0.0-rc7, a folder whose parent folder the cmdlet hadn't reported was left out of the result, so the
# entries of the parent here were missing.
It 'Should report all entries of a folder whose parent folder it did not report' {
$childAlone = @(Get-NTFSSimpleAccess -Path $child -IncludeRootFolder:$false -ErrorAction Stop)
$parentAlone = @(Get-NTFSSimpleAccess -Path $parent -IncludeRootFolder:$false -ErrorAction Stop)
$parentAlone | Should -Not -BeNullOrEmpty
$result = @(Get-NTFSSimpleAccess -Path $child, $parent -IncludeRootFolder:$false -ErrorAction Stop)
@($result | Where-Object -Property FullName -EQ -Value $child) | Should -HaveCount $childAlone.Count
@($result | Where-Object -Property FullName -EQ -Value $parent) | Should -HaveCount $parentAlone.Count
}
# Before 5.0.0-rc7, a folder that came after its parent folder a second time failed with a ReadError, "An item
# with the same key has already been added."
It 'Should compare a folder that it gets twice with its parent folder both times' {
$result = @(Get-NTFSSimpleAccess -Path $parent, $child, $child -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
$simpleErrors | Should -BeNullOrEmpty
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
$childEntries | Should -HaveCount 2
$childEntries | ForEach-Object -Process { $_.Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101' }
}
# Before 5.0.0-rc7, a drive root after the first path was left out as well, because it has no parent folder;
# after another folder whose parent was reported, it was compared with that unrelated parent.
It 'Should report all entries of a drive root, which has no parent folder' {
$root = [IO.Path]::GetPathRoot($child)
$result = @(Get-NTFSSimpleAccess -Path $child, $root -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
$simpleErrors | Should -BeNullOrEmpty
@($result | Where-Object -Property FullName -EQ -Value $root) | Should -Not -BeNullOrEmpty
}
It 'Should report the parent folder of the first path first by default' {
$result = @(Get-NTFSSimpleAccess -Path $child -ErrorAction Stop)

Loading…
Cancel
Save