Browse Source

Merge pull request #105 from raandree/ai/issue-fixes

feat!: fix six reported issues; -Attributes matches any listed attribute, Size alias removed
pull/112/head
Raimund Andrée 6 days ago
committed by GitHub
parent
commit
c8ecf66ba3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      .memory-bank/activeContext.md
  2. 15
      .memory-bank/progress.md
  3. 29
      CHANGELOG.md
  4. 4
      Docs/Cmdlets/Copy-Item2.md
  5. 8
      Docs/Cmdlets/Get-ChildItem2.md
  6. 2
      Docs/Cmdlets/Get-NTFSEffectiveAccess.md
  7. 2
      Docs/Cmdlets/Remove-NTFSAccess.md
  8. 2
      Docs/Concepts.md
  9. 64
      Docs/FAQ.md
  10. 3
      Docs/README.md
  11. 1
      NTFSSecurity/AccessCmdlets/AddAccess.cs
  12. 1
      NTFSSecurity/AccessCmdlets/ClearAccess.cs
  13. 3
      NTFSSecurity/AccessCmdlets/GetAccess.cs
  14. 3
      NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs
  15. 1
      NTFSSecurity/AccessCmdlets/RemoveAccess.cs
  16. 1
      NTFSSecurity/AuditCmdlets/AddAudit.cs
  17. 1
      NTFSSecurity/AuditCmdlets/ClearAudit.cs
  18. 3
      NTFSSecurity/AuditCmdlets/GetAudit.cs
  19. 1
      NTFSSecurity/AuditCmdlets/RemoveAudit.cs
  20. 114
      NTFSSecurity/BaseCmdlets.cs
  21. 1
      NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs
  22. 1
      NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs
  23. 1
      NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs
  24. 1
      NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs
  25. 3
      NTFSSecurity/InheritanceCmdlets/GetInheritance.cs
  26. 1
      NTFSSecurity/InheritanceCmdlets/SetInheritance.cs
  27. 10
      NTFSSecurity/ItemCmdlets/CopyItem2.cs
  28. 14
      NTFSSecurity/ItemCmdlets/GetChildItem2.cs
  29. 3
      NTFSSecurity/ItemCmdlets/GetItem2.cs
  30. 2
      NTFSSecurity/ItemCmdlets/MoveItem2.cs
  31. 1
      NTFSSecurity/ItemCmdlets/RemoveItem2.cs
  32. 3
      NTFSSecurity/LinkCmdlets/GetHardLink.cs
  33. 2
      NTFSSecurity/LinkCmdlets/NewHardLink.cs
  34. 2
      NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs
  35. 1
      NTFSSecurity/MiscCmdlets/GetFileHash2.cs
  36. 4
      NTFSSecurity/NTFSSecurity.types.ps1xml
  37. 1
      NTFSSecurity/OwnerCmdlets/GetOwner.cs
  38. 1
      NTFSSecurity/OwnerCmdlets/SetOwner.cs
  39. 3
      NTFSSecurity/PathCmdlets/TestPath2.cs
  40. 3
      NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs
  41. 17
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  42. 76
      Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs
  43. 76
      Tests/Access.Tests.ps1
  44. 56
      Tests/ItemCmdlets.Tests.ps1
  45. 28
      Tests/Manifest.Tests.ps1
  46. 74
      Tests/Owner.Tests.ps1

6
.memory-bank/activeContext.md

@ -61,6 +61,10 @@ the PRs, and tags `5.0.0-rc2` after the merges.
395 passed, 26 skipped; PowerShell 7 366 passed, 55 skipped (421
tests). `Get-FileHash2` tests now run in PowerShell 7 as well.
- `ai/issue-fixes` fixes #3, #86, and #88 and adds `Docs/FAQ.md`: Windows
PowerShell 402 passed, 26 skipped; PowerShell 7 373 passed, 55 skipped
(428 tests).
## Next step
The bugs from the issue triage (`ai/issue-fixes`), then 5.0.0-rc2.
5.0.0-rc2 on `ai/release-5.0.0-rc2`.

15
.memory-bank/progress.md

@ -179,3 +179,18 @@ Numbered as agreed with the maintainer; each is documented on its page.
`MACTripleDES` uses a random key (verified), so it is deprecated.
- Review of group E: the changelog now marks the `Set-NTFSInheritance`
change as breaking and warns that it leaves broader access in place.
#### Issue triage (fixes on `ai/issue-fixes`, not merged)
- Fixed: #3 (braces in a path), #86 (`$PWD` shadowed, also for the default
location of nine cmdlets, found by the review), #88 (an object passed by
position), #17 (an entry with `GenericAll`); `Docs/FAQ.md` answers the
recurring questions.
- Review of #17: generic rights are removed the way .NET removes other
rights, an exact match as it is, otherwise without `Synchronize`.
- Maintainer decisions of 2026-10-05: #5 (`Get-ChildItem2 -Attributes`
matches any listed attribute) and #82 (no `Size` alias) ship in 5.0.0 as
breaking changes. Their review added that an empty `-Attributes` value is
an error, as in `Get-ChildItem`.
- Open bugs: #34 and #67 (writes owner and group, rc3 if a file server is
available), #41 (drive root) and #90 (trailing space), both after 5.0.0.

29
CHANGELOG.md

@ -49,6 +49,19 @@ The format is based on
before. To remove the entries, use
`Disable-NTFSAccessInheritance -RemoveInheritedAccessRules` or
`Enable-NTFSAuditInheritance -RemoveExplicitAuditRules`
- **Breaking:** `Get-ChildItem2 -Attributes` returns the items that have any
of the listed attributes, like `Get-ChildItem`; it returned only the items
that had all of them. A call that lists several attributes now returns
more items, including hidden and system items when those are in the
list, so review calls whose result is deleted or whose permissions are
changed. To get the old result, filter with `Where-Object`, as the cmdlet
page shows. An empty value, such as `0`, is now an error; it returned
every item, also the hidden ones
([#5](https://github.com/raandree/NTFSSecurity/issues/5))
- **Breaking:** remove the alias `Size` of `LengthOnDisk` from the files of
`Get-ChildItem`, which made the import fail in Windows PowerShell when
another module had added a `Size` member; use `LengthOnDisk`
([#82](https://github.com/raandree/NTFSSecurity/issues/82))
### Deprecated
@ -169,5 +182,21 @@ The format is based on
`RIPEMD160` and `MACTripleDES`, which .NET lacks there, now stop the
cmdlet with an error that names the algorithm and points to Windows
PowerShell 5.1
- Fix a `FormatException` in the cmdlets for a path with braces, such as
`C:\Data\{Archive}`: their messages formatted the path a second time
([#3](https://github.com/raandree/NTFSSecurity/issues/3))
- Fix a `NullReferenceException` in every cmdlet when a variable named
`PWD` in the scope of the caller, such as a loop variable, hid the
automatic variable; the cmdlets now read the current location from the
session, and only for a relative path
([#86](https://github.com/raandree/NTFSSecurity/issues/86))
- Fix file and folder objects passed by position, such as
`Get-NTFSOwner $folder`, which Windows PowerShell bound as the name of the
item, so the cmdlets looked for it in the current location
([#88](https://github.com/raandree/NTFSSecurity/issues/88))
- Fix `Remove-NTFSAccess` for an entry with a generic right such as
`GenericAll`, which Windows keeps in the inherit-only entries of folders;
it failed with "The value '269484032' is not valid"
([#17](https://github.com/raandree/NTFSSecurity/issues/17))
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

4
Docs/Cmdlets/Copy-Item2.md

@ -178,11 +178,11 @@ You can pipe an object that has a `Destination` property to supply the target of
### Alphaleonis.Win32.Filesystem.FileInfo
By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied.
By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied, pointing at the copy.
### Alphaleonis.Win32.Filesystem.DirectoryInfo
With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied.
With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied, pointing at the copy.
## NOTES

8
Docs/Cmdlets/Get-ChildItem2.md

@ -57,19 +57,19 @@ PS C:\> Get-ChildItem2 -Path C:\Data -Recurse -Depth 1 -Filter '*.log'
Returns the log files in `C:\Data` and in its immediate subfolders. Without `-Depth`, the command would descend through the entire tree.
### Example 4: List hidden system files
### Example 4: List hidden or system files
```PowerShell
PS C:\> dir2 -Path C:\Data -Attributes Hidden, System
```
Uses the `dir2` alias and returns the items of `C:\Data` that have both the hidden and the system attribute.
Uses the `dir2` alias and returns the items of `C:\Data` that have the hidden or the system attribute, like `Get-ChildItem -Attributes Hidden, System`.
## PARAMETERS
### -Attributes
Specifies a set of file attributes. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`.
Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones.
```yaml
Type: FileAttributes
@ -307,7 +307,7 @@ The `PrivateData` section of the module manifest `NTFSSecurity.psd1` contains tw
A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors.
Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`.
Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones.
## RELATED LINKS

2
Docs/Cmdlets/Get-NTFSEffectiveAccess.md

@ -164,7 +164,7 @@ One or more paths of files or folders, piped by value or by the property `FullNa
### Security2.FileSystemSecurity2[]
Security descriptors are accepted by the parameter binder but produce no result in this cmdlet.
One or more security descriptors that `Get-NTFSSecurityDescriptor` returned. The cmdlet calculates the effective access from the descriptor in memory instead of reading the item again.
### Security2.IdentityReference2

2
Docs/Cmdlets/Remove-NTFSAccess.md

@ -304,6 +304,8 @@ If the ACL of an item cannot be written because access is denied, the cmdlet tri
Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged.
An entry with a generic right, such as `GenericAll`, can be removed, for example by piping it from `Get-NTFSAccess`. Windows keeps generic rights in the inherit-only entries of folders. Before 5.0.0, the cmdlet failed for such an entry with the error "The value '269484032' is not valid for this usage of the type FileSystemRights".
Before 5.0.0, the `-RemoveSpecific` switch was missing, although version 4.1 had introduced it.
A path that does not exist produces the non-terminating error `ReadFileError`, and the cmdlet continues with the next path. Before 5.0.0, the cmdlet also wrote a misleading `RemoveAceError` for that path, and with `-PassThru` it stopped with a `NullReferenceException`.

2
Docs/Concepts.md

@ -234,7 +234,7 @@ the AlphaFS objects that the `*-Item2` cmdlets return.
| --- | --- | --- | --- |
| `Owner` | Property | Files and folders | The owner of the item. |
| `IsInheritanceBlocked` | Property | Files and folders | `$true` if the item does not inherit access entries. |
| `LengthOnDisk` | Property | Files | The file size rounded up to whole clusters of the volume. `Size` is an alias. |
| `LengthOnDisk` | Property | Files | The file size rounded up to whole clusters of the volume. Before 5.0.0, `Size` was an alias. |
| `EnableInheritance()` | Method | Files and folders | Turns on access inheritance. |
| `DisableInheritance()` | Method | Files and folders | Turns off access inheritance. Pass `$false` to drop the inherited entries instead of copying them. |
| `GetHash()` | Method | Files | Returns the SHA1 hash of the file as a hexadecimal string. |

64
Docs/FAQ.md

@ -0,0 +1,64 @@
# Frequently asked questions
Answers to questions that come up again and again in the issues. For the
background, read [Concepts](Concepts.md); for longer scripts, read
[Examples](Examples.md).
## The module fails to load with HRESULT 0x80131515
Windows blocks the assemblies of a ZIP file that was downloaded from the
internet. Install the module from the PowerShell Gallery instead, as
described in [Installation](README.md#installation), or unblock the files of
a downloaded copy with `Get-ChildItem -Recurse | Unblock-File` before you
import it.
## Access is denied, although I am an administrator
Run PowerShell elevated. Only an elevated session holds the Backup,
Restore, Take Ownership, and Security privileges, which the cmdlets enable
to read and change items that your account has no rights on. Reading or
changing audit entries always needs the Security privilege. See
[Privileges](Concepts.md#privileges) and the module setting
`EnablePrivileges` in [Module settings](Concepts.md#module-settings).
## Get-NTFSEffectiveAccess shows other rights than Explorer
`Get-NTFSEffectiveAccess` calculates the rights that the NTFS permissions
of the item grant to one account. It doesn't include share permissions,
which Explorer adds for a path on a file share, and the account must be
resolvable on the computer that runs the cmdlet. See
[Get-NTFSEffectiveAccess](Cmdlets/Get-NTFSEffectiveAccess.md).
## Get-ChildItem2 -Recurse runs in a loop through junctions
A junction can point to a folder above it. Use `-SkipMountPoints` and
`-SkipSymbolicLinks` to leave out junctions and symbolic links when you
walk a tree, for example before you pipe the items to `Get-NTFSAccess`.
See [Get-ChildItem2](Cmdlets/Get-ChildItem2.md).
## How do I restore permissions that I exported?
`Get-NTFSAccess` returns the account, the rights, the type, and the
inheritance and propagation flags of each entry. `Add-NTFSAccess` binds
`-Account`, `-AccessRights`, `-AccessType`, `-InheritanceFlags`, and
`-PropagationFlags` by property name, so the objects, or the rows of a CSV
file with these columns and the path, can be piped back to it. See
[Add-NTFSAccess](Cmdlets/Add-NTFSAccess.md).
## How do I apply the same audit entries to a whole folder tree?
Add the entry to the top folder only. By default, `Add-NTFSAudit` applies
it to the folder, its subfolders, and its files, so the items below inherit
it. To remove other entries from the items below, use `Clear-NTFSAudit`,
and use `Enable-NTFSAuditInheritance` where inheritance is blocked.
Changing audit entries needs an elevated session. See
[Add-NTFSAudit](Cmdlets/Add-NTFSAudit.md).
## Can I use a PowerShell drive in a path?
No. The cmdlets read and write the file system directly through the AlphaFS
library, not through the PowerShell providers, so they don't know drives
that `New-PSDrive` created, or drives of other providers such as `HKLM:`.
Use the file system path instead, such as `C:\Data` or `\\server\share`. A
relative path is resolved against the current file system location. See
[Long paths](Concepts.md#long-paths).

3
Docs/README.md

@ -72,7 +72,8 @@ Get-NTFSAccess -Path C:\Windows
Read [Concepts](Concepts.md) for the background and [Examples](Examples.md)
for common tasks. Every cmdlet has a reference page with all parameters and
examples; see the [cmdlet list](#cmdlets).
examples; see the [cmdlet list](#cmdlets). The [FAQ](FAQ.md) answers
questions that come up again and again.
## Cmdlets

1
NTFSSecurity/AccessCmdlets/AddAccess.cs

@ -23,6 +23,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/AccessCmdlets/ClearAccess.cs

@ -13,6 +13,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

3
NTFSSecurity/AccessCmdlets/GetAccess.cs

@ -20,6 +20,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -73,7 +74,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
}

3
NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs

@ -20,6 +20,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -108,7 +109,7 @@ namespace NTFSSecurity
}
// Like the other cmdlets, use the current location when -Path is omitted.
var targets = paths.Count > 0 ? paths : new List<string>() { GetVariableValue("PWD").ToString() };
var targets = paths.Count > 0 ? paths : new List<string>() { GetCurrentLocation() };
foreach (var path in targets)
{

1
NTFSSecurity/AccessCmdlets/RemoveAccess.cs

@ -24,6 +24,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/AuditCmdlets/AddAudit.cs

@ -23,6 +23,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/AuditCmdlets/ClearAudit.cs

@ -13,6 +13,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

3
NTFSSecurity/AuditCmdlets/GetAudit.cs

@ -20,6 +20,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -73,7 +74,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
}

1
NTFSSecurity/AuditCmdlets/RemoveAudit.cs

@ -24,6 +24,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "PathComplex")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

114
NTFSSecurity/BaseCmdlets.cs

@ -26,32 +26,7 @@ namespace NTFSSecurity
#region GetFileSystemInfo
protected System.IO.FileSystemInfo GetFileSystemInfo(string path)
{
string currentLocation = GetVariableValue("PWD").ToString();
if (path == ".")
{
path = currentLocation;
}
if (path.StartsWith(".."))
{
path = System.IO.Path.Combine(
string.Join("\\", currentLocation.Split('\\').Take(currentLocation.Split('\\').Count() - path.Split('\\').Count(s => s == "..")).ToArray()),
string.Join("\\", path.Split('\\').Where(e => e != "..").ToArray()));
}
else if (path.StartsWith("."))
{
//combine . and .\path\subpath
path = System.IO.Path.Combine(currentLocation, path.Substring(2));
}
else if (path.StartsWith("\\"))
{
//do nothing
}
else
{
////combine . and \path\subpath or path\subpath
path = System.IO.Path.Combine(currentLocation, path.Substring(0));
}
path = GetRelativePath(path);
if (System.IO.File.Exists(path))
{
@ -114,18 +89,17 @@ namespace NTFSSecurity
#region GetRelativePath
protected string GetRelativePath(string path)
{
string currentLocation = GetVariableValue("PWD").ToString();
if (string.IsNullOrEmpty(path))
{
path = currentLocation;
path = GetCurrentLocation();
}
else if (path == ".")
{
path = currentLocation;
path = GetCurrentLocation();
}
else if (path.StartsWith(".."))
{
var currentLocation = GetCurrentLocation();
path = System.IO.Path.Combine(
string.Join("\\", currentLocation.Split('\\').Take(currentLocation.Split('\\').Count() - path.Split('\\').Count(s => s == "..")).ToArray()),
string.Join("\\", path.Split('\\').Where(e => e != "..").ToArray()));
@ -133,20 +107,31 @@ namespace NTFSSecurity
else if (path.StartsWith("."))
{
//combine . and .\path\subpath
path = System.IO.Path.Combine(currentLocation, path.Substring(2));
path = System.IO.Path.Combine(GetCurrentLocation(), path.Substring(2));
}
else if (path.StartsWith("\\"))
else if (path.StartsWith("\\") || System.IO.Path.IsPathRooted(path))
{
//do nothing
//an absolute path needs no location
}
else
{
////combine . and \path\subpath or path\subpath
path = System.IO.Path.Combine(currentLocation, path);
////combine . and path\subpath
path = System.IO.Path.Combine(GetCurrentLocation(), path);
}
return path;
}
/// <summary>
/// Returns the current file system location of the session. It is read from the session state, not from
/// $PWD, which a variable named PWD in the scope of the caller can hide (#86). In a location of another
/// provider, such as the registry, this is the last file system location.
/// </summary>
/// <returns>The provider path of the current file system location.</returns>
protected string GetCurrentLocation()
{
return SessionState.Path.CurrentFileSystemLocation.ProviderPath;
}
#endregion
#region InvokeAsOwner
@ -337,18 +322,71 @@ namespace NTFSSecurity
WriteDebug("The privilige 'Security' was disabled.");
}
// These overloads hide the single-argument methods of Cmdlet, so a message without arguments must not be
// formatted: a path with braces in it would make string.Format throw (#3).
protected void WriteWarning(string text, params string[] args)
{
base.WriteWarning(string.Format(text, args));
base.WriteWarning(args == null || args.Length == 0 ? text : string.Format(text, args));
}
protected void WriteVerbose(string text, params string[] args)
{
base.WriteVerbose(string.Format(text, args));
base.WriteVerbose(args == null || args.Length == 0 ? text : string.Format(text, args));
}
protected void WriteDebug(string text, params string[] args)
{
base.WriteDebug(string.Format(text, args));
base.WriteDebug(args == null || args.Length == 0 ? text : string.Format(text, args));
}
}
/// <summary>
/// Converts file and folder objects to their full path. Windows PowerShell binds an object that is passed by
/// position to a string parameter through ToString, which returns only the name of a child item, so the cmdlet
/// resolved it against the current location (#88).
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class FileSystemPathTransformationAttribute : ArgumentTransformationAttribute
{
/// <summary>
/// Returns the full path of a file or folder object, or of each one in a collection, and any other value
/// unchanged.
/// </summary>
/// <param name="engineIntrinsics">The engine APIs of the session.</param>
/// <param name="inputData">The argument to transform.</param>
/// <returns>The transformed argument.</returns>
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
var input = inputData is PSObject ? ((PSObject)inputData).BaseObject : inputData;
if (input is string || !(input is IEnumerable))
{
return ToPath(inputData);
}
var result = new List<object>();
foreach (var item in (IEnumerable)input)
{
result.Add(ToPath(item));
}
return result.ToArray();
}
private static object ToPath(object value)
{
var baseObject = value is PSObject ? ((PSObject)value).BaseObject : value;
if (baseObject is System.IO.FileSystemInfo)
{
return ((System.IO.FileSystemInfo)baseObject).FullName;
}
if (baseObject is Alphaleonis.Win32.Filesystem.FileSystemInfo)
{
return ((Alphaleonis.Win32.Filesystem.FileSystemInfo)baseObject).FullName;
}
return value;
}
}
}

1
NTFSSecurity/InheritanceCmdlets/DisableAccessInheritance.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/InheritanceCmdlets/DisableAuditInheritance.cs

@ -16,6 +16,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/InheritanceCmdlets/EnableAccessInheritance.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/InheritanceCmdlets/EnableAuditInheritance.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

3
NTFSSecurity/InheritanceCmdlets/GetInheritance.cs

@ -13,6 +13,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -41,7 +42,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
}

1
NTFSSecurity/InheritanceCmdlets/SetInheritance.cs

@ -16,6 +16,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

10
NTFSSecurity/ItemCmdlets/CopyItem2.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -26,6 +27,7 @@ namespace NTFSSecurity
}
[Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[FileSystemPathTransformation]
public string Destination
{
get { return destination; }
@ -95,12 +97,13 @@ namespace NTFSSecurity
try
{
var processed = false;
FileSystemInfo copy = null;
if (item is FileInfo)
{
if (ShouldProcess(resolvedPath, "Copy File"))
{
((FileInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath);
copy = ((FileInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath);
WriteVerbose(string.Format("File '{0}' copied to '{1}'", resolvedPath, actualDestination));
processed = true;
}
@ -112,14 +115,15 @@ namespace NTFSSecurity
// AlphaFS 2.2 copies into an existing folder only and otherwise fails with a
// DirectoryNotFoundException for the first file.
Directory.CreateDirectory(actualDestination);
((DirectoryInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath);
copy = ((DirectoryInfo)item).CopyTo(actualDestination, force ? CopyOptions.None : CopyOptions.FailIfExists, PathFormat.RelativePath);
WriteVerbose(string.Format("Directory '{0}' copied to '{1}'", resolvedPath, actualDestination));
processed = true;
}
}
// Write the object for the copy that CopyTo returns, not the source item.
if (passThru && processed)
WriteObject(item);
WriteObject(copy);
}
catch (System.IO.IOException ex)
{

14
NTFSSecurity/ItemCmdlets/GetChildItem2.cs

@ -33,6 +33,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -131,9 +132,17 @@ namespace NTFSSecurity
{
base.BeginProcessing();
// An empty value would match every item, also the hidden ones; Get-ChildItem rejects it as well.
if (MyInvocation.BoundParameters.ContainsKey("Attributes") && attributes == 0)
{
ThrowTerminatingError(new ErrorRecord(
new ArgumentException("Specify at least one file attribute for the Attributes parameter."),
"AttributesEmpty", ErrorCategory.InvalidArgument, attributes));
}
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
wildcard = new WildcardPattern(filter, WildcardOptions.Compiled | WildcardOptions.IgnoreCase);
@ -274,7 +283,8 @@ namespace NTFSSecurity
if (MyInvocation.BoundParameters.ContainsKey("Attributes"))
{
if ((current.Attributes & attributes) != attributes)
// Like Get-ChildItem, an item matches when it has any of the listed attributes (#5).
if ((current.Attributes & attributes) == 0)
continue;
writeItem = true;

3
NTFSSecurity/ItemCmdlets/GetItem2.cs

@ -13,6 +13,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -29,7 +30,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode");

2
NTFSSecurity/ItemCmdlets/MoveItem2.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -26,6 +27,7 @@ namespace NTFSSecurity
}
[Parameter(Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[FileSystemPathTransformation]
public string Destination
{
get { return destination; }

1
NTFSSecurity/ItemCmdlets/RemoveItem2.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

3
NTFSSecurity/LinkCmdlets/GetHardLink.cs

@ -14,6 +14,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -30,7 +31,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
modeMethodInfo = typeof(FileSystemCodeMembers).GetMethod("Mode");

2
NTFSSecurity/LinkCmdlets/NewHardLink.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string Path
{
get { return paths[0]; }
@ -27,6 +28,7 @@ namespace NTFSSecurity
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[FileSystemPathTransformation]
public string Target
{
get { return target; }

2
NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string Path
{
get { return paths[0]; }
@ -27,6 +28,7 @@ namespace NTFSSecurity
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[FileSystemPathTransformation]
public string Target
{
get { return target; }

1
NTFSSecurity/MiscCmdlets/GetFileHash2.cs

@ -16,6 +16,7 @@ namespace NTFSSecurity
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

4
NTFSSecurity/NTFSSecurity.types.ps1xml

@ -23,10 +23,6 @@
[Math]::Ceiling($this.Length / ($driveInfo.BytesPerSector * $driveInfo.SectorsPerCluster)) * ($driveInfo.BytesPerSector * $driveInfo.SectorsPerCluster)
</GetScriptBlock>
</ScriptProperty>
<AliasProperty>
<Name>Size</Name>
<ReferencedMemberName>LengthOnDisk</ReferencedMemberName>
</AliasProperty>
<ScriptMethod>
<Name>EnableInheritance</Name>
<Script>

1
NTFSSecurity/OwnerCmdlets/GetOwner.cs

@ -12,6 +12,7 @@ namespace NTFSSecurity.OwnerCmdlets
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

1
NTFSSecurity/OwnerCmdlets/SetOwner.cs

@ -15,6 +15,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }

3
NTFSSecurity/PathCmdlets/TestPath2.cs

@ -13,6 +13,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -36,7 +37,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths = new List<string>() { GetVariableValue("PWD").ToString() };
paths = new List<string>() { GetCurrentLocation() };
}
}

3
NTFSSecurity/SecurityDescriptorCmdlets/GetSecurityDescriptor.cs

@ -12,6 +12,7 @@ namespace NTFSSecurity
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string[] Path
{
get { return paths.ToArray(); }
@ -28,7 +29,7 @@ namespace NTFSSecurity
if (paths.Count == 0)
{
paths.Add(GetVariableValue("PWD").ToString());
paths.Add(GetCurrentLocation());
}
}

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

@ -2166,7 +2166,7 @@ PS C:\&gt; Set-NTFSSecurityDescriptor -SecurityDescriptor $sd</dev:code>
<maml:name>Alphaleonis.Win32.Filesystem.FileInfo</maml:name>
</dev:type>
<maml:description>
<maml:para>By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied.</maml:para>
<maml:para>By default this cmdlet returns nothing. With `-PassThru $true` it returns a file object for each file that it copied, pointing at the copy.</maml:para>
</maml:description>
</command:returnValue>
<command:returnValue>
@ -2174,7 +2174,7 @@ PS C:\&gt; Set-NTFSSecurityDescriptor -SecurityDescriptor $sd</dev:code>
<maml:name>Alphaleonis.Win32.Filesystem.DirectoryInfo</maml:name>
</dev:type>
<maml:description>
<maml:para>With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied.</maml:para>
<maml:para>With `-PassThru $true` the cmdlet returns a folder object for each folder that it copied, pointing at the copy.</maml:para>
</maml:description>
</command:returnValue>
</command:returnValues>
@ -3524,7 +3524,7 @@ PS C:\&gt; Disable-Privileges</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>Attributes</maml:name>
<maml:description>
<maml:para>Specifies a set of file attributes. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`.</maml:para>
<maml:para>Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones.</maml:para>
</maml:description>
<command:parameterValueGroup>
<command:parameterValue required="false" command:variableLength="false">ReadOnly</command:parameterValue>
@ -3668,7 +3668,7 @@ PS C:\&gt; Disable-Privileges</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>Attributes</maml:name>
<maml:description>
<maml:para>Specifies a set of file attributes. The cmdlet returns only the items that have all the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`.</maml:para>
<maml:para>Specifies a set of file attributes. Like `Get-ChildItem`, the cmdlet returns the items that have at least one of the attributes you list; separate several values with commas, as in `-Attributes Hidden, System`. Unlike `Get-ChildItem`, the parameter takes only such a list, not the `+` and `!` operators; to get only the items that have all the attributes, filter the result, for example with `Where-Object { ($_.Attributes -band [IO.FileAttributes]'Hidden, System') -eq [IO.FileAttributes]'Hidden, System' }`. An empty value, such as `0`, stops the cmdlet with the error `AttributesEmpty`. When you use this parameter, the cmdlet ignores `-Force`, `-Hidden`, `-System`, and `-ReadOnly`, and it returns matching hidden items without `-Force`. The parameter restricts the returned items only; `-Recurse` still descends into every subfolder, including hidden ones.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">FileAttributes</command:parameterValue>
<dev:type>
@ -3857,7 +3857,7 @@ PS C:\&gt; Disable-Privileges</dev:code>
<maml:para>The default table view shows the `Mode`, `Inherits`, `LastWriteTime`, `Size(M)`, and `Name` columns. `Inherits` is `False` for an item whose access inheritance is disabled. Reading that value costs one access to the ACL of each displayed item, which slows down the display of large listings; to avoid it, select the properties you need, for example with `Format-Table -Property Mode, LastWriteTime, Length, Name`. Objects that you pipe to another command are not affected. Before 5.0.0, the column showed `True` for every item.</maml:para>
<maml:para>The `PrivateData` section of the module manifest `NTFSSecurity.psd1` contains two settings that this cmdlet reads when it starts. `GetFileSystemModeProperty` adds the calculated `Mode` property to every item. `IdentifyHardLinks` adds the `HardLinkCount` property to every file, which requires an extra call into the file system for each file and therefore slows down large listings noticeably. Set either value to `$false` in the manifest and import the module again if you prefer the faster enumeration over the additional properties.</maml:para>
<maml:para>A folder that cannot be read produces a non-terminating error with the ID `DirUnauthorizedAccessError` for an access denial or `DirUnspecifiedError` for any other failure, and a path that does not exist produces the error `FileNotFound`. In each case the cmdlet continues with the next path. Failures that occur while `-Recurse` collects the subfolders of a folder are reported as verbose messages only, not as errors.</maml:para>
<maml:para>Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`.</maml:para>
<maml:para>Before 5.0.0, a `-Path` value that points to a file stopped the cmdlet with an `InvalidCastException`, `-Attributes` returned only the items that had all the listed attributes, and an empty `-Attributes` value returned every item, also the hidden ones.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
@ -3883,10 +3883,10 @@ PS C:\&gt; Disable-Privileges</dev:code>
</dev:remarks>
</command:example>
<command:example>
<maml:title>------------- Example 4: List hidden system files -------------</maml:title>
<maml:title>------------ Example 4: List hidden or system files ------------</maml:title>
<dev:code>PS C:\&gt; dir2 -Path C:\Data -Attributes Hidden, System</dev:code>
<dev:remarks>
<maml:para>Uses the `dir2` alias and returns the items of `C:\Data` that have both the hidden and the system attribute.</maml:para>
<maml:para>Uses the `dir2` alias and returns the items of `C:\Data` that have the hidden or the system attribute, like `Get-ChildItem -Attributes Hidden, System`.</maml:para>
</dev:remarks>
</command:example>
</command:examples>
@ -5117,7 +5117,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:name>Security2.FileSystemSecurity2[]</maml:name>
</dev:type>
<maml:description>
<maml:para>Security descriptors are accepted by the parameter binder but produce no result in this cmdlet.</maml:para>
<maml:para>One or more security descriptors that `Get-NTFSSecurityDescriptor` returned. The cmdlet calculates the effective access from the descriptor in memory instead of reading the item again.</maml:para>
</maml:description>
</command:inputType>
<command:inputType>
@ -8416,6 +8416,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>If the ACL of an item cannot be written because access is denied, the cmdlet tries once more after making the current account the owner of the item, and restores the previous owner afterwards. Changing the owner of an item requires the Take Ownership and Restore privileges, so this fallback only succeeds in an elevated session of an account that holds them.</maml:para>
<maml:para>Removing rights from an entry that does not exist is not an error; the cmdlet leaves the ACL unchanged.</maml:para>
<maml:para>An entry with a generic right, such as `GenericAll`, can be removed, for example by piping it from `Get-NTFSAccess`. Windows keeps generic rights in the inherit-only entries of folders. Before 5.0.0, the cmdlet failed for such an entry with the error "The value '269484032' is not valid for this usage of the type FileSystemRights".</maml:para>
<maml:para>Before 5.0.0, the `-RemoveSpecific` switch was missing, although version 4.1 had introduced it.</maml:para>
<maml:para>A path that does not exist produces the non-terminating error `ReadFileError`, and the cmdlet continues with the next path. Before 5.0.0, the cmdlet also wrote a misleading `RemoveAceError` for that path, and with `-PassThru` it stopped with a `NullReferenceException`.</maml:para>
</maml:alert>

76
Security2/FileSystem/FileSystemAccessRule2 Class/FileSystemAccessRule2.RemoveFileSystemAccessRules.cs

@ -1,11 +1,52 @@
using Alphaleonis.Win32.Filesystem;
using System.Collections.Generic;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Principal;
namespace Security2
{
public partial class FileSystemAccessRule2
{
// Rights that FileSystemAccessRule.AccessMaskFromRights rejects, such as the generic rights, which Windows
// keeps in the inherit-only entries of folders (#17).
private static bool HasUnsupportedRights(int accessMask)
{
return accessMask < 0 || accessMask > (int)FileSystemRights.FullControl;
}
// FileSystemSecurity.RemoveAccessRule removes a rule that matches an entry exactly as it is, and otherwise
// rebuilds it without the Synchronize right, which fails for unsupported rights. For those, do the same
// without rebuilding the rule.
private static void RemoveRule(FileSystemSecurity sd, FileSystemAccessRule ace, bool removeSpecific)
{
var accessMask = (int)ace.FileSystemRights;
if (!HasUnsupportedRights(accessMask))
{
if (removeSpecific)
sd.RemoveAccessRuleSpecific(ace);
else
sd.RemoveAccessRule(ace);
return;
}
var sid = (SecurityIdentifier)ace.IdentityReference.Translate(typeof(SecurityIdentifier));
var exactMatch = sd.GetAccessRules(true, true, typeof(SecurityIdentifier))
.OfType<FileSystemAccessRule>()
.Any(rule => (int)rule.FileSystemRights == accessMask &&
rule.IdentityReference == sid &&
rule.AccessControlType == ace.AccessControlType);
var ruleToRemove = exactMatch ? ace : (FileSystemAccessRule)sd.AccessRuleFactory(sid,
accessMask & ~(int)FileSystemRights.Synchronize, false, ace.InheritanceFlags, ace.PropagationFlags, ace.AccessControlType);
// Like RemoveAccessRule, ignore whether an entry was changed: removing an entry that doesn't exist is not
// an error.
bool modified;
sd.ModifyAccessRule(removeSpecific ? AccessControlModification.RemoveSpecific : AccessControlModification.Remove, ruleToRemove, out modified);
}
public static void RemoveFileSystemAccessRule(FileSystemInfo item, IdentityReference2 account, FileSystemRights2 rights, AccessControlType type, InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, bool removeSpecific = false)
{
if (type == AccessControlType.Allow)
@ -19,10 +60,7 @@ namespace Security2
var sd = file.GetAccessControl(AccessControlSections.Access);
ace = (FileSystemAccessRule)sd.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type);
if (removeSpecific)
sd.RemoveAccessRuleSpecific(ace);
else
sd.RemoveAccessRule(ace);
RemoveRule(sd, ace, removeSpecific);
file.SetAccessControl(sd);
}
@ -33,10 +71,7 @@ namespace Security2
var sd = directory.GetAccessControl(AccessControlSections.Access);
ace = (FileSystemAccessRule)sd.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type);
if (removeSpecific)
sd.RemoveAccessRuleSpecific(ace);
else
sd.RemoveAccessRule(ace);
RemoveRule(sd, ace, removeSpecific);
directory.SetAccessControl(sd);
}
@ -85,10 +120,7 @@ namespace Security2
var file = (FileInfo)item;
var sd = file.GetAccessControl(AccessControlSections.Access);
if (removeSpecific)
sd.RemoveAccessRuleSpecific(ace);
else
sd.RemoveAccessRule(ace);
RemoveRule(sd, ace, removeSpecific);
file.SetAccessControl(sd);
}
@ -98,10 +130,7 @@ namespace Security2
var sd = directory.GetAccessControl(AccessControlSections.Access);
if (removeSpecific)
sd.RemoveAccessRuleSpecific(ace);
else
sd.RemoveAccessRule(ace);
RemoveRule(sd, ace, removeSpecific);
directory.SetAccessControl(sd);
}
@ -113,20 +142,7 @@ namespace Security2
rights = rights | FileSystemRights2.Synchronize;
var ace = (FileSystemAccessRule)sd.SecurityDescriptor.AccessRuleFactory(account, (int)rights, false, inheritanceFlags, propagationFlags, type);
if (sd.IsFile)
{
if (removeSpecific)
((FileSecurity)sd.SecurityDescriptor).RemoveAccessRuleSpecific(ace);
else
((FileSecurity)sd.SecurityDescriptor).RemoveAccessRule(ace);
}
else
{
if (removeSpecific)
((DirectorySecurity)sd.SecurityDescriptor).RemoveAccessRuleSpecific(ace);
else
((DirectorySecurity)sd.SecurityDescriptor).RemoveAccessRule(ace);
}
RemoveRule(sd.SecurityDescriptor, ace, removeSpecific);
return ace;
}

76
Tests/Access.Tests.ps1

@ -94,6 +94,15 @@ Describe 'Get-NTFSEffectiveAccess' {
}
Describe 'Get-NTFSOrphanedAccess' {
# Before 5.0.0, a path with braces stopped the cmdlet with a FormatException (#3).
It 'Should read a folder whose name contains braces' {
$braces = Join-Path -Path $sandbox -ChildPath ('{{Braces}}-{0}' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
Assert-TestSandboxPath -Sandbox $sandbox -Path $braces
[IO.Directory]::CreateDirectory($braces) | Out-Null
{ Get-NTFSOrphanedAccess -Path $braces -ErrorAction Stop } | Should -Not -Throw
}
BeforeAll {
$orphanedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'Orphaned'
Assert-TestSandboxPath -Sandbox $sandbox -Path $orphanedFile
@ -199,6 +208,73 @@ Describe 'Remove-NTFSAccess' {
}
}
Context 'With a generic right' {
# Before 5.0.0, removing an entry with a generic right such as GENERIC_ALL failed with "The value '269484032' is
# not valid", because .NET rebuilds the rule and rejects generic rights (#17). Windows keeps generic rights in
# inherit-only entries of folders.
BeforeAll {
$guests = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-546'
function New-GenericRightFolder {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the sandbox.'
)]
param ([string] $Entry)
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Generic' -Directory
Assert-TestSandboxPath -Sandbox $sandbox -Path $folder
$acl = Get-Acl -LiteralPath $folder
$acl.SetSecurityDescriptorSddlForm(($acl.Sddl -replace 'D:(?<flags>[A-Z]*)', ('D:${flags}' + $Entry)))
Set-Acl -LiteralPath $folder -AclObject $acl
$folder
}
function Get-GuestsRule {
param ([string] $Path)
(Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) |
Where-Object -Property IdentityReference -EQ -Value $guests
}
}
It 'Should remove an inherit-only entry that Get-NTFSAccess returned, and nothing else' -ForEach @(
@{ Case = 'Allow'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $false }
@{ Case = 'Allow with -RemoveSpecific'; Entry = '(A;OICIIO;GA;;;BG)'; Specific = $true }
@{ Case = 'Deny'; Entry = '(D;OICIIO;GA;;;BG)'; Specific = $false }
) {
$folder = New-GenericRightFolder -Entry $Entry
$before = (Get-Acl -LiteralPath $folder).Sddl
$before | Should -Match ([regex]::Escape($Entry))
Get-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -ExcludeInherited |
Remove-NTFSAccess -RemoveSpecific:$Specific -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -BeNullOrEmpty
(Get-Acl -LiteralPath $folder).Sddl | Should -BeExactly $before.Replace($Entry, '')
}
It 'Should remove only the requested generic right from an entry with two' {
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x90000000;;;BG)'
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericRead -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -BeNullOrEmpty
$rule = Get-GuestsRule -Path $folder
$rule | Should -HaveCount 1
[int] $rule.FileSystemRights | Should -Be 0x10000000
}
# A rule that matches the entry exactly is removed as it is, with the Synchronize right that the module adds
# to an Allow rule; otherwise an entry with only Synchronize would be left behind.
It 'Should remove an entry with GenericAll and Synchronize when -AccessRights names GenericAll' {
$folder = New-GenericRightFolder -Entry '(A;OICIIO;0x10100000;;;BG)'
Remove-NTFSAccess -Path $folder -Account 'S-1-5-32-546' -AccessRights GenericAll -InheritanceFlags ContainerInherit, ObjectInherit -PropagationFlags InheritOnly -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -BeNullOrEmpty
Get-GuestsRule -Path $folder | Should -BeNullOrEmpty
}
}
Context 'With -RemoveSpecific' {
BeforeEach {
$removeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveSpecific' -Directory

56
Tests/ItemCmdlets.Tests.ps1

@ -78,6 +78,37 @@ Describe 'Get-ChildItem2' {
($lines | Where-Object -FilterScript { $_ -match 'Inheriting\.txt\s*$' }) | Should -Match '\bTrue\b'
}
}
Context 'With -Attributes' {
BeforeAll {
$attributeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'Attributes' -Directory
$hiddenFile = Join-Path -Path $attributeFolder -ChildPath 'Hidden.txt'
$readOnlyFile = Join-Path -Path $attributeFolder -ChildPath 'ReadOnly.txt'
$plainFile = Join-Path -Path $attributeFolder -ChildPath 'Plain.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $hiddenFile, $readOnlyFile, $plainFile
Set-Content -LiteralPath $hiddenFile, $readOnlyFile, $plainFile -Value 'Attributes'
(Get-Item -LiteralPath $hiddenFile -Force).Attributes = [IO.FileAttributes]::Hidden
(Get-Item -LiteralPath $readOnlyFile).Attributes = [IO.FileAttributes]::ReadOnly
}
# Before 5.0.0, the cmdlet returned only the items that had all the listed attributes (#5).
It 'Should return the items that have any of the listed attributes, like Get-ChildItem' {
$result = @(Get-ChildItem2 -Path $attributeFolder -Attributes Hidden, ReadOnly)
@($result.Name | Sort-Object) | Should -Be @('Hidden.txt', 'ReadOnly.txt')
}
# Before 5.0.0, an empty value applied no filter and returned hidden items as well.
It 'Should reject an empty value' {
{ Get-ChildItem2 -Path $attributeFolder -Attributes 0 -ErrorAction Stop } | Should -Throw -ErrorId 'AttributesEmpty,NTFSSecurity.GetChildItem2'
}
It 'Should return only the items with the attribute when one is listed' {
$result = @(Get-ChildItem2 -Path $attributeFolder -Attributes ReadOnly)
$result.Name | Should -Be 'ReadOnly.txt'
}
}
}
Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' {
@ -129,6 +160,31 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' {
$messages.Message | Should -Contain ("File '{0}' {1} to '{2}'" -f $first, $Verb, $target)
}
# With -PassThru, both cmdlets return the item at the destination, as their pages say.
It 'Copy-Item2 -PassThru should return the copy' {
$result = Copy-Item2 -Path $first -Destination $destination -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'First.txt')
$first | Should -Exist
}
It 'Copy-Item2 -PassThru should return the copy of a folder' {
$sourceFolder = Join-Path -Path $folder -ChildPath 'SourceFolder'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder
New-Item -ItemType Directory -Path $sourceFolder | Out-Null
Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'Inner.txt') -Value 'Inner'
$result = Copy-Item2 -Path $sourceFolder -Destination (Join-Path -Path $destination -ChildPath 'Copied') -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'Copied')
$sourceFolder | Should -Exist
}
It 'Move-Item2 -PassThru should return the item at its new location' {
$result = Move-Item2 -Path $first -Destination $destination -PassThru $true
$result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'First.txt')
}
# Before 5.0.0, -PassThru wrote the item also when -WhatIf skipped the operation.
It '<_> should write nothing with -PassThru and -WhatIf' -ForEach @('Copy-Item2', 'Move-Item2', 'Remove-Item2') {
$parameters = @{ Path = $first; PassThru = $true; WhatIf = $true }

28
Tests/Manifest.Tests.ps1

@ -65,3 +65,31 @@ Describe 'Module manifest of NTFSSecurity' {
}
}
}
Describe 'Type data of NTFSSecurity' {
BeforeDiscovery {
# Only Windows PowerShell fails to import type data that conflicts with an existing member, so both CI legs
# start it.
$windowsPowerShell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
}
BeforeAll {
$windowsPowerShell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
}
# Before 5.0.0, the types file added the alias Size to System.IO.FileInfo, so the import failed when another module
# had added a member with that name (#82). The module is imported in a child process, because type data stays in a
# session.
It 'Should import in Windows PowerShell after another module added a Size member to System.IO.FileInfo' -Skip:(-not (Test-Path -LiteralPath $windowsPowerShell)) {
$manifestPath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
$manifestPath | Should -Exist
$quotedPath = $manifestPath.Replace("'", "''")
$command = 'Update-TypeData -TypeName System.IO.FileInfo -MemberType AliasProperty -MemberName Size -Value Length -Force; ' +
("Import-Module -Name '{0}' -ErrorAction Stop; " -f $quotedPath) +
("if ((Get-Item -LiteralPath '{0}').PSObject.Properties['LengthOnDisk']) {{ 'IMPORTED' }}" -f $quotedPath)
$output = & $windowsPowerShell -NoProfile -NonInteractive -Command $command 2>&1
$output | Select-Object -Last 1 | Should -Be 'IMPORTED'
}
}

74
Tests/Owner.Tests.ps1

@ -4,6 +4,9 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidAssignmentToAutomaticVariable', '', Justification = 'A test shadows $PWD on purpose (#86).'
)]
param ()
BeforeDiscovery {
@ -52,3 +55,74 @@ Describe 'Get-NTFSOwner' {
}
}
}
Describe 'Current location' {
BeforeAll {
# The command runs in a child scope of this function, so the cmdlets see its $PWD = $null through the scope
# chain, as in the report.
function Invoke-WithShadowedPwd {
param ([scriptblock] $Command)
$PWD = $null
& $Command
}
}
# Before 5.0.0, a variable named PWD in the scope of the caller, such as a loop variable, made every cmdlet
# fail with a NullReferenceException, also for an absolute path (#86).
It 'Should ignore a variable named PWD for an absolute path' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Pwd'
$result = Invoke-WithShadowedPwd -Command { Get-NTFSOwner -Path $file -ErrorAction Stop }
$result.FullName | Should -Be $file
}
It 'Should resolve a relative path against the current location despite a variable named PWD' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'PwdRelative'
$name = Split-Path -Path $file -Leaf
$result = Invoke-WithShadowedPwd -Command { Get-NTFSOwner -Path $name -ErrorAction Stop }
$result.FullName | Should -Be $file
}
It '<_> should use the current location without -Path despite a variable named PWD' -ForEach @(
'Get-NTFSAccess', 'Get-NTFSAudit', 'Get-NTFSEffectiveAccess', 'Get-NTFSInheritance', 'Get-ChildItem2',
'Get-Item2', 'Get-NTFSSecurityDescriptor'
) {
$cmdlet = $_
{ Invoke-WithShadowedPwd -Command { & $cmdlet -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } } |
Should -Not -Throw
}
It 'Get-NTFSHardLink should report the folder of the current location, not a NullReferenceException' {
{ Invoke-WithShadowedPwd -Command { Get-NTFSHardLink -ErrorAction SilentlyContinue } } |
Should -Throw -ExpectedMessage '*must be a file*'
}
}
Describe 'File and folder objects as arguments' {
# Before 5.0.0, Windows PowerShell bound a folder object that was passed by position as its name, which the
# cmdlets resolved against the current location (#88).
It 'Should take a folder object by position' {
$parent = New-TestSandboxItem -Sandbox $sandbox -Name 'Parent' -Directory
$child = Join-Path -Path $parent -ChildPath 'Child'
Assert-TestSandboxPath -Sandbox $sandbox -Path $child
New-Item -ItemType Directory -Path $child | Out-Null
$folder = Get-ChildItem -LiteralPath $parent -Directory
$result = Get-NTFSOwner $folder -ErrorAction Stop
$result.FullName | Should -Be $child
}
It 'Should take file objects through the pipeline as before' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Piped'
$result = Get-Item -LiteralPath $file | Get-NTFSOwner
$result.FullName | Should -Be $file
}
}

Loading…
Cancel
Save