Browse Source

Merge pull request #116 from raandree/ai/release-5.0.0-rc7

Release 5.0.0-rc7: the behavior changes of Phase 2
pull/121/head
Raimund Andrée 2 days ago
committed by GitHub
parent
commit
8a6be9fc82
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 112
      .memory-bank/activeContext.md
  2. 53
      .memory-bank/decisions/0022-phase-2-behavior-changes.md
  3. 114
      .memory-bank/progress.md
  4. 18
      .memory-bank/systemPatterns.md
  5. 39
      CHANGELOG.md
  6. 4
      Docs/Cmdlets/Get-NTFSEffectiveAccess.md
  7. 8
      Docs/Cmdlets/Get-NTFSOrphanedAudit.md
  8. 4
      Docs/Cmdlets/Get-NTFSSimpleAccess.md
  9. 6
      Docs/Cmdlets/Move-Item2.md
  10. 26
      Docs/Cmdlets/New-NTFSHardLink.md
  11. 28
      Docs/Cmdlets/New-NTFSSymbolicLink.md
  12. 18
      Docs/FAQ.md
  13. 7
      NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs
  14. 12
      NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs
  15. 6
      NTFSSecurity/AuditCmdlets/GetAudit.cs
  16. 19
      NTFSSecurity/ItemCmdlets/MoveItem2.cs
  17. 121
      NTFSSecurity/LinkCmdlets/NewHardLink.cs
  18. 76
      NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs
  19. 2
      NTFSSecurity/NTFSSecurity.psd1
  20. 53
      NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs
  21. 77
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  22. 35
      Security2/Win32/Lib.cs
  23. 76
      Tests/Access.Tests.ps1
  24. 24
      Tests/Audit.Tests.ps1
  25. 63
      Tests/ItemCmdlets.Tests.ps1
  26. 27
      Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md
  27. 149
      Tests/Lab/Acceptance-2026-10-08-5.0.0-rc7.md
  28. 7
      Tests/Lab/NTFSSecurity.Live.Tests.ps1
  29. 3
      Tests/Lab/README.md
  30. 168
      Tests/Links.Tests.ps1
  31. 2
      Tests/Repository.Tests.ps1
  32. 36
      Tests/TestHelpers.Tests.ps1
  33. 43
      Tests/TestHelpers.psm1

112
.memory-bank/activeContext.md

@ -9,70 +9,62 @@ source: current task evidence
## Current focus ## Current focus
Phase 2 of the quality gate before 5.0.0 (Decision 21) is complete on the 5.0.0-rc6 is on the PowerShell Gallery (tag `5.0.0-rc6` on `b51d970`, the
local branch `ai/release-5.0.0-rc6`, candidate `7b0781f`. The maintainer merge of #115); its GitHub release waits for a rerun of the failed Release
pushes the branch, merges the pull request after CI, and tags 5.0.0-rc6. job. #116 (`ai/release-5.0.0-rc7`, base `master`) holds the behavior
Then Phase 3 (live tests on more operating systems) and 5.0.0; after changes that Phase 2 found, decided as assumptions for the maintainer's
5.0.0 the repository is archived in favor of WindowsAccessControl review (Decision 22), and waits for that review. Then 5.0.0-rc7, Phase 3,
(Decision 18). and 5.0.0; after 5.0.0 the repository is archived in favor of
WindowsAccessControl (Decision 18).
## Evidence ## Evidence
- 2026-10-08, Phase 2 on `ai/release-5.0.0-rc6`, 31 commits on `fcb370e` - 2026-10-08, 5.0.0-rc6: the Release job of the tag (run `37839669028`)
(5.0.0-rc5); the candidate is `7b0781f`: published the package at 20:40 UTC and failed after it, because
- The suite has 684 tests. Elevated: 662 passed and 22 skipped in `Publish-PSResource` gave up waiting after 100 seconds and its retry got
Windows PowerShell 5.1, 632 and 52 in PowerShell 7. As a basic user 409 (`progress.md`, open work 8). The live tests of rc7 ran with
through `Invoke-TestsAsBasicUser.ps1`: 590 and 94, 560 and 124. No `-Version 5.0.0-rc6`, which checks the hash of the Gallery, in both
failure, and no test is skipped in all four configurations; CI runs editions, 20:43 to 21:00 UTC: all passed except the warning text that
all four since this branch. rc7 changed, which matches the live tests of rc6. The fixture was
- C# coverage of all four configurations (AltCover without `--save`, removed at 21:03 UTC and its removal checked.
`techContext.md`): 68.1% of the lines and 44.3% of the branches on - 2026-10-08, #116, 11 commits on `be04cb7` (`3899228` to `1063b29`) and
`1b9edbb`; rc5 58.1% and 38.0%. The earlier figures counted one of commits of records:
the four runs. Of the 972 points that no test ran at `e2b6b24` - Decision 22: items 1, 2, 5, 6, 7, and 8 changed, 7 and 8 breaking (the
(without the classes that no cmdlet calls), 400 are code that nothing link cmdlets require `-Path` and `-Target` and write non-terminating
calls, 107 defensive guards, 74 need a failure of Windows, 4 need the errors); items 3, 4, 9, and 10 kept, 9 with an FAQ entry. New defects,
lab, and 387 are reachable; 51 of those ran in runs that the first fixed with a regression test that failed first: `Move-Item2` deleted
measurement missed, and the top items of the rest are in an empty folder that it moved to another volume (AlphaFS emulated the
`progress.md`, open work 7. move); the link cmdlets failed with `GetDefaultValueFailed` for every
- Fixed test-first: `Get-NTFSSimpleAccess` (`ReadData`, the parent of a piped object; `Get-NTFSSimpleAccess` failed for a folder that came
relative path); `Copy-Item2` and `Move-Item2` (a folder at the after its parent folder a second time.
destination, the missing destination folder of #21, also with - One `security-reviewer` pass over `be04cb7..4ee01e5`: no Blocker or
`-WhatIf`); the hard-link cmdlets on shares and at folders; Major. Minor 1 to 5 and Nits 7 to 9 fixed test-first in `7936d9f` to
`Set-NTFSSecurityDescriptor -PassThru` (R5); the error ID of `1063b29`; Nit 7, the warning of `Get-NTFSEffectiveAccess` for names
`Get-NTFSOrphanedAccess`; relative paths that start with a dot, which of this computer, was reproduced first. Nit 6 declined (Decision 22).
every cmdlet shortened by two characters (`Remove-Item2 .x` removed - Suite of `1063b29`: 712 tests. Elevated: 688 passed and 24 skipped in
another item); comparing entries and descriptors Windows PowerShell 5.1, 658 and 54 in PowerShell 7. As a basic user:
(`InvalidCastException`, also `Compare-Object` in PowerShell 7) and 612 and 100, 582 and 130. No failure, none skipped in all four.
their conversions; `InheritedFrom` with `-ExcludeExplicit` and for a - Lab acceptance of `dc6e9f5` after the checkpoint
descriptor with audit entries (`ArgumentOutOfRangeException`). `ntfs-rc7-dc6e9f5-before-acceptance`, 16:24 to 16:40 UTC: 326 tests in
`Copy-Item2` no longer creates the missing folders of a destination both editions, none failed, 2 skipped as in rc6
for a folder (assumption, flagged for the maintainer). (`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc7.md`). The code of
- Live tests: cases 4b to 9 and accounts of `b.forest1.net`, `4ee01e5` and a first run of `dc6e9f5` without the checkpoint had the
`forest2.net`, and `forest3.net`. The lab acceptance passed for same counts. The fixture was removed at 16:21 and 16:44 UTC, and its
`acfe3af`, `1b9edbb`, and `7b0781f` (326 tests each, none failed; removal checked each time.
`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`). Baseline: the - #115 passed CI in all four configurations on `be04cb7`, with the first
published 5.0.0-rc5 fails only the two hard-link tests of case 8. The runs of `Invoke-TestsAsBasicUser.ps1` on GitHub runners; #116 passed CI
fixture was removed and the removal checked after each run; three on `ebe91fe` against the rc6 branch.
checkpoints `ntfs-rc6-*-before-acceptance` stay on six machines.
- `Get-NTFSEffectiveAccess -ServerName` needs administrators or
Access Control Assistance Operators on the named computer (lab probe,
documented).
- Two `security-reviewer` passes approved the branch with no Blocker or
Major finding: `fcb370e..e2b6b24` (findings 1, 4, 5, 10 fixed in
`acfe3af`) and `e2b6b24..1b9edbb` (findings 1, 2, 6, 7 fixed in
`7b0781f`). Not fixed: a failed privilege disable isn't tried again,
and a non-qualified ACE could shift `InheritedFrom` (neither
reproducible); `New-NTFSHardLink` stays terminating for a folder (a
behavior change for the maintainer).
- #34: no reply from the tester since 2026-10-06. - #34: no reply from the tester since 2026-10-06.
## Next step ## Next step
1. The maintainer pushes the branch, opens the pull request, merges it 1. The maintainer reruns the failed Release job of 5.0.0-rc6, which skips
after CI, and tags `5.0.0-rc6`; then the live tests run against the the published package and creates the GitHub release, and closes #110.
published package (`-Version 5.0.0-rc6`). The first CI run is the first 2. He pushes the records to #116 and reviews the choices of Decision 22,
run of `Invoke-TestsAsBasicUser.ps1` on a GitHub runner. each its own commit, above all the two breaking changes of the link
2. The maintainer decides the behavior changes in `progress.md`, open cmdlets. After the CI of the push, he merges #116 with a merge commit
work 4 (Decision 16), and the scope of Phase 3: the operating systems, (Decision 15) and tags `5.0.0-rc7`; the live tests then run against the
the code that nothing calls, and file servers that aren't Windows published package (`-Version 5.0.0-rc7`).
(#34). 3. He decides the fix of the publish step (`progress.md`, open work 8) and
the scope of Phase 3: the operating systems, the code that nothing
calls, and file servers that aren't Windows (#34).

53
.memory-bank/decisions/0022-phase-2-behavior-changes.md

@ -0,0 +1,53 @@
---
status: proposed
date: 2026-10-08
last-verified: 2026-10-08
owner: shared
source: agent choices in autopilot on 2026-10-08, for the maintainer's review
---
# Decision 22: The behavior changes of Phase 2
- Context: Decision 16 left the behavior changes that Phase 2 found to the
maintainer (`progress.md`, open work 4). On 2026-10-08 he asked to work
on them while the pull request of 5.0.0-rc6 (#115) built, in autopilot;
the agent took the recommended option for each. Every choice is an
assumption for his review. Each change is its own commit on
`ai/release-5.0.0-rc7` (`3899228` to `4ee01e5`, the label in `d17e0f7`,
the review fixes in `7936d9f` to `1063b29`). A revert can conflict where
a later commit touched the same page or test, and then needs the help
file generated again.
- Choices:
| # | Item | Choice |
| --- | --- | --- |
| 1 | `Get-NTFSOrphanedAudit` returned nothing without the Security privilege | Changed: `ReadSecurityError`, like `Get-NTFSAudit`; a missing path keeps `ReadError` |
| 2 | `Get-NTFSSimpleAccess` left out a folder whose parent it hadn't reported | Fixed: such a folder, also a drive root, gets all entries; a repeated folder no longer fails with `ReadError` |
| 3 | Developer Mode for `New-NTFSSymbolicLink` | Kept as documented: new P/Invoke and fallback code before the archive (Decision 18) |
| 4 | `-WhatIf` names a conflict in a verbose message, not a warning (R7) | Kept, as for #108 and the missing destination folder |
| 5 | The fallback warning of `Get-NTFSEffectiveAccess` didn't name the server | Changed: it names the computer |
| 6 | `Move-Item2` can't move a folder to another volume | Kept the refusal; fixed what was found: with `CopyAllowed`, AlphaFS copied and deleted folders and lost empty ones. Now a `MoveError` that names folder and destination |
| 7 | The link cmdlets stopped with terminating errors | **Breaking:** a non-terminating error per link, and the next link |
| 8 | `-Path` and `-Target` of the link cmdlets were optional | **Breaking:** required. An omitted `-Path` failed with an index error, an omitted `-Target` meant the current location |
| 9 | Entries and descriptors are equal only as the same .NET object | Kept the equality of .NET; the FAQ shows `Compare-Object -Property` |
| 10 | `Copy-Item2` doesn't create the missing destination folders (rc6) | Kept, like `Copy-Item` and `Move-Item2` |
- Found on the way and fixed: every object piped to the link cmdlets
failed with `GetDefaultValueFailed` (item 8). Item 6 is not the cause of
#21, whose report used `-Force` within one volume.
- Review: one `security-reviewer` pass over `be04cb7..4ee01e5` found no
Blocker or Major issue. Fixed test-first: the link cmdlets stopped for a
path with an invalid character in Windows PowerShell, named no path in
some errors, and checked `-Path` and `-Target` in different orders;
`Get-NTFSEffectiveAccess` warned for every name of this computer except
`localhost` in lowercase (reproduced, Decision 16); the tests of the
case-insensitive parent lookup and of a drive root, and the shared
administrative-share helpers. Declined: one error ID for a missing path
in the audit cmdlets (`ReadError` and `ReadFileError`), a change of
behavior for scripts that check the ID.
- Rationale: an error instead of a result that looks valid (1, 2, 6);
per-item errors, as in the other cmdlets (7); no silent default for a
path that creates something (8); no new features before the archive
(3); the conventions of .NET and PowerShell (4, 9, 10).
- Open: the maintainer accepts or reverts each choice; then this record
becomes `accepted`.

114
.memory-bank/progress.md

@ -9,15 +9,16 @@ source: repository evidence
## Current status ## Current status
5.0.0-rc5 is on the PowerShell Gallery and in the GitHub releases, 5.0.0-rc6 is on the PowerShell Gallery, published by CI on 2026-10-08 at
published by CI on 2026-10-08 from the tag `5.0.0-rc5` on `master` 20:40 UTC from the tag `5.0.0-rc6` on `master` (`b51d970`, the merge of
(`fcb370e`, the merge of #114; Decision 12). Phase 2 of the quality gate pull request #115; Decision 12). The Release job failed after the upload,
(Decision 21) is complete on the local branch `ai/release-5.0.0-rc6`: the so the GitHub release waits for a rerun of the failed job. The published
candidate 5.0.0-rc6 (`acfe3af`) passed the suite in four configurations, package passed the live tests. Phase 2 of the quality gate (Decision 21)
one `security-reviewer` pass, and the lab acceptance. It waits for the is complete. The pull request #116 (`ai/release-5.0.0-rc7`) holds the
maintainer to push, merge, and tag it; Phase 3 follows. The stable Gallery behavior changes that Phase 2 found, decided as assumptions for the
version is still 4.2.6. NTFSSecurity will be archived soon; its users move maintainer's review (Decision 22), and waits for that review. Phase 3
to WindowsAccessControl (Decision 18). follows. The stable Gallery version is still 4.2.6. NTFSSecurity will be
archived soon; its users move to WindowsAccessControl (Decision 18).
## Recent milestones ## Recent milestones
@ -100,6 +101,23 @@ to WindowsAccessControl (Decision 18).
measured again; the first measurements counted one of four runs). Two measured again; the first measurements counted one of four runs). Two
`security-reviewer` passes; the lab acceptance of `7b0781f` passed `security-reviewer` passes; the lab acceptance of `7b0781f` passed
(`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`). (`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`).
- 2026-10-08: #115 (rc6, head `be04cb7`) passed CI in all four
configurations. On `ai/release-5.0.0-rc7` (local), the behavior changes
of Phase 2 were decided as assumptions for review (Decision 22) and
implemented test-first, two of them breaking (the link cmdlets); new
defects found on the way: `Move-Item2` deleted an empty folder that it
moved to another volume, the link cmdlets failed for every piped object,
and `Get-NTFSEffectiveAccess` warned for names of this computer. One
`security-reviewer` pass (no Blocker or Major; its findings fixed but
one, declined). Suite and lab acceptance in `activeContext.md`.
- 2026-10-08: #115 merged (`b51d970`) and tagged `5.0.0-rc6`. The Release
job published the package at 20:40 UTC, then failed: `Publish-PSResource`
gave up waiting after 100 seconds while the Gallery accepted the upload,
and its retry got 409, so the job didn't create the GitHub release. The
published package passed the live tests of rc7 in both editions except
the one test whose expected warning text rc7 changed
(`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`, After the release).
#116 (5.0.0-rc7) was opened on the rc6 branch and moved to `master`.
## Stable capabilities ## Stable capabilities
@ -114,20 +132,22 @@ to WindowsAccessControl (Decision 18).
## Open work ## Open work
1. Quality gate before 5.0.0 (Decision 21): Phase 2 is done on the branch 1. Quality gate before 5.0.0 (Decision 21): the maintainer reruns the
`ai/release-5.0.0-rc6` (`activeContext.md`); the maintainer pushes it, failed Release job of 5.0.0-rc6, which creates the GitHub release;
merges the pull request, and tags 5.0.0-rc6, and the live tests run reviews the choices of Decision 22 in #116; merges #116 and tags
against the published package. Phase 3 runs the live tests on more 5.0.0-rc7, whose published package then runs the live tests. Phase 3
operating systems. Then release runs the live tests on more operating systems. Then release 5.0.0
5.0.0 through CI (Decision 12): remove the label, date `[Unreleased]` as through CI (Decision 12): remove the label, date
`[5.0.0]`, add the last prerelease to `$publishedVersions`, and tag `[Unreleased]` as `[5.0.0]`, add the last prerelease to
`5.0.0` (steps in `Docs/Contributing/05-Releasing.md`). #34 stays open `$publishedVersions`, and tag `5.0.0` (steps in
with Bug and Help Wanted until a tester with a file server that refuses `Docs/Contributing/05-Releasing.md`). #34 stays open with Bug and Help
the owner confirms the fix, or until 5.0.0 ships. Wanted until a tester with a file server that refuses the owner
2. Issues: the rc6 branch addresses the seven items of #110 (tests); the confirms the fix, or until 5.0.0 ships.
pull request names it without a closing keyword, so the maintainer 2. Issues: 5.0.0-rc6 addresses the seven items of #110 (tests); #115
closes it after the merge. #21 (a misleading error of `Move-Item2`) got named it without a closing keyword, so the maintainer closes it now.
a fix in rc6 that names the missing destination folder. #68 #21 (a misleading error of `Move-Item2`) got
a fix in rc6 that names the missing destination folder; the folder
moves to another volume that rc7 fixes are a different defect. #68
tracks `-WhatIf` and `-Confirm` for every cmdlet that changes security. tracks `-WhatIf` and `-Confirm` for every cmdlet that changes security.
The labels follow Decision 17; #16, #21, #45, and #89 wait for their The labels follow Decision 17; #16, #21, #45, and #89 wait for their
reporters (Needs Info). Not planned for 5.0.0: the enhancements #22, reporters (Needs Info). Not planned for 5.0.0: the enhancements #22,
@ -138,32 +158,17 @@ to WindowsAccessControl (Decision 18).
them the accounts filter that `RemoveFileSystemAccessRuleAll` and them the accounts filter that `RemoveFileSystemAccessRuleAll` and
`RemoveFileSystemAuditRuleAll` ignore, which no cmdlet passes; of rc5, `RemoveFileSystemAuditRuleAll` ignore, which no cmdlet passes; of rc5,
the bare `catch` in `Win32.GetEffectiveAccess`, the unchecked the bare `catch` in `Win32.GetEffectiveAccess`, the unchecked
`AUTHZ_ACCESS_REPLY.Error`, a fallback warning without the server name, `AUTHZ_ACCESS_REPLY.Error`, and hardening of the lab controller (guards
and hardening of the lab controller (guards in the setup blocks, in the setup blocks, interpolated `-EncodedCommand` paths, CredSSP by
interpolated `-EncodedCommand` paths, CredSSP by IP address, the IP address, the password string in memory, disabling the role accounts
password string in memory, disabling the role accounts after a run); of after a run); of rc6, a privilege that fails to be disabled isn't tried
rc6, a privilege that fails to be disabled isn't tried again by again by `Dispose` (finding 2, not reproducible); of rc7, one error ID
`Dispose` (finding 2, not reproducible). for a missing path in the audit cmdlets (declined, Decision 22).
4. Behavior changes found in Phase 2, for the maintainer (Decision 16): 4. Behavior changes found in Phase 2 (Decision 16): decided in Decision 22
`Get-NTFSOrphanedAudit` returns nothing without the Security privilege, as assumptions for the maintainer's review, on `ai/release-5.0.0-rc7`;
while `Get-NTFSAudit` writes `ReadSecurityError`; two of them are breaking changes of the link cmdlets. Left for Phase 3:
`Get-NTFSSimpleAccess` skips a folder whose parent it didn't process; 400 points of code that nothing calls besides the 244 lines of unused
`New-NTFSSymbolicLink` could create links without the privilege in classes.
Developer Mode (flag `SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE`, with
a fallback before Windows 10 1703); `-WhatIf` names a conflict in a
verbose message instead of a warning (R7); the fallback warning of
`Get-NTFSEffectiveAccess` doesn't name the server; `Move-Item2` can't
move a folder to another volume; `New-NTFSHardLink` stops with a
terminating error for a folder, unlike `Get-NTFSHardLink` (rc6 review,
finding 11). Taken as an assumption in rc6, for review: `Copy-Item2`
no longer creates the missing folders of a destination for a folder.
Found by the coverage report of rc6: `-Target` of the link cmdlets is
optional and means the current location when it's omitted; equality of
entries and descriptors is the identity of the wrapped .NET object, so
two reads of the same entry differ for `Compare-Object` and
`Select-Object -Unique` (value equality would be a behavior change); 400
points of code that nothing calls besides the 244 lines of unused
classes (Phase 3).
5. `pwsh` 7.6.1 crashed three times during test runs on the ARM64 5. `pwsh` 7.6.1 crashed three times during test runs on the ARM64
workstation (x64 emulation), without module frames; none of the CI runs workstation (x64 emulation), without module frames; none of the CI runs
on native x64 on 2026-10-05 crashed. on native x64 on 2026-10-05 crashed.
@ -171,8 +176,8 @@ to WindowsAccessControl (Decision 18).
GitHub authorization, restrict wiki editing to collaborators, ask GitHub authorization, restrict wiki editing to collaborators, ask
`Sup3rlativ3` to delete the Read the Docs project, and delete the branch `Sup3rlativ3` to delete the Read the Docs project, and delete the branch
`test/transfer`. In the lab, delete the checkpoints `test/transfer`. In the lab, delete the checkpoints
`ntfs-rc6-*-before-acceptance` of the six machines when they are no `ntfs-rc6-*-before-acceptance` and `ntfs-rc7-*-before-acceptance` of the
longer needed. six machines when they are no longer needed.
7. Reachable code that no test runs (coverage report of rc6, ranked by 7. Reachable code that no test runs (coverage report of rc6, ranked by
impact; about 300 points): `Remove-Item2` on folders (`-Recurse`, impact; about 300 points): `Remove-Item2` on folders (`-Recurse`,
`-Force`, `DeleteError`); the owner restore after taking ownership `-Force`, `DeleteError`); the owner restore after taking ownership
@ -189,3 +194,10 @@ to WindowsAccessControl (Decision 18).
`Set-NTFSInheritance`. A display limit, not a defect: a conditional ACE `Set-NTFSInheritance`. A display limit, not a defect: a conditional ACE
shows as an unconditional entry, because the .NET rules have no shows as an unconditional entry, because the .NET rules have no
condition. condition.
8. The publish step of the Release job fails when `Publish-PSResource`
gives up waiting after 100 seconds while the Gallery accepts the
package, because its retry gets 409 (5.0.0-rc6). Proposed for the
maintainer (Decision 16, not reproducible on demand; he was asked on
2026-10-08 and didn't answer, so it stays open): treat the error as
success when `Find-PSResource` then lists the version, in a script with
Pester tests. Until then, rerun the failed job.

18
.memory-bank/systemPatterns.md

@ -76,9 +76,27 @@ Each Decision record is a file in `decisions/`; read only the relevant ones.
| 19 | [Cmdlets write only the sections that they change](decisions/0019-write-only-changed-sections.md) | | 19 | [Cmdlets write only the sections that they change](decisions/0019-write-only-changed-sections.md) |
| 20 | [Live tests in a lab live in Tests\Lab](decisions/0020-live-tests-in-tests-lab.md) | | 20 | [Live tests in a lab live in Tests\Lab](decisions/0020-live-tests-in-tests-lab.md) |
| 21 | [A quality gate before 5.0.0](decisions/0021-quality-gate-before-5.0.0.md) | | 21 | [A quality gate before 5.0.0](decisions/0021-quality-gate-before-5.0.0.md) |
| 22 | [The behavior changes of Phase 2 (proposed)](decisions/0022-phase-2-behavior-changes.md) |
## Patterns ## Patterns
### Writing cmdlets
- A parameter that takes pipeline input needs a getter that doesn't
throw: PowerShell reads it before it binds each input object, and an
exception turns every object into `GetDefaultValueFailed` (the link
cmdlets before 5.0.0-rc7).
- An error for one item is non-terminating, so that the cmdlet goes on
with the next path or pipeline object; since 5.0.0-rc7, the link cmdlets
too. Its message names the item, and its target object is the item that
the cmdlet was asked to process. Resolving a path can throw in Windows
PowerShell for an invalid character, so that belongs inside the
per-item error handling.
- Folders move without `MoveOptions.CopyAllowed`: for another volume,
AlphaFS then copies and deletes, which lost empty folders. Windows
refuses such a move with `NotSameDeviceException` (17). Tests reach
another volume through `\\localhost\C$`, elevated only.
### Verifying documentation ### Verifying documentation
- Run platyPS in Windows PowerShell 5.1 against a Release build; a copy of - Run platyPS in Windows PowerShell 5.1 against a Release build; a copy of

39
CHANGELOG.md

@ -77,6 +77,18 @@ The format is based on
administrators of the named computer and the members of its group Access administrators of the named computer and the members of its group Access
Control Assistance Operators; any other account gets the error "Access is Control Assistance Operators; any other account gets the error "Access is
denied" and no result denied" and no result
- **Breaking:** require `-Path` and `-Target` in `New-NTFSHardLink` and
`New-NTFSSymbolicLink`. Without `-Path`, they failed with an index error;
without `-Target`, they used the current location, so
`New-NTFSSymbolicLink -Path Link` created a link to the current folder
- **Breaking:** write a non-terminating error in `New-NTFSHardLink` and
`New-NTFSSymbolicLink` for a link that they can't create, such as for an
existing `-Path`, a missing `-Target`, or a path with a character that
Windows doesn't allow, and continue with the next link; they stopped
with a terminating error. A script that relies on the stop needs
`-ErrorAction Stop`
- Name the computer in the warning of `Get-NTFSEffectiveAccess` when the
computer of `-ServerName` can't be reached
### Deprecated ### Deprecated
@ -328,5 +340,32 @@ The format is based on
entry, and `Get-NTFSAccess -SecurityDescriptor` stopped with an entry, and `Get-NTFSAccess -SecurityDescriptor` stopped with an
`ArgumentOutOfRangeException` for a descriptor with audit entries, such `ArgumentOutOfRangeException` for a descriptor with audit entries, such
as one that `Get-NTFSSecurityDescriptor` reads in an elevated session as one that `Get-NTFSSecurityDescriptor` reads in an elevated session
- Fix `Get-NTFSOrphanedAudit`, which returned nothing without the Security
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, and a parent folder is found
also when its path differs in case. A folder that came after its parent
folder a second time failed with a `ReadError`
- Fix `Move-Item2` for a folder on another volume, which Windows can't
move: the cmdlet copied and deleted it instead, so that an empty folder
was deleted without being created at the destination, and a folder with
files failed with an error that named one of its files. It now writes a
`MoveError` that names the folder and the destination, and leaves the
folder in place
- Fix `New-NTFSHardLink` and `New-NTFSSymbolicLink`, which failed with
`GetDefaultValueFailed` for every object piped to them, such as the rows
of a CSV file with the columns `Path` and `Target`
- Fix the errors of `New-NTFSSymbolicLink` for an existing `-Path` and a
missing `-Target`, and of `New-NTFSHardLink` for a folder as `-Target`,
which named no path. `New-NTFSSymbolicLink` now checks `-Path` first,
like `New-NTFSHardLink`
- Fix `Get-NTFSEffectiveAccess`, which warned that the result might be
inaccurate for every name of this computer in `-ServerName` except
`localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name,
where the computer doesn't offer the remote access check
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

4
Docs/Cmdlets/Get-NTFSEffectiveAccess.md

@ -31,7 +31,7 @@ Calculates the rights an account really has on a file or a folder and writes the
The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports. The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports.
When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled. When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.
When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again. When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again.
@ -184,6 +184,8 @@ Reading effective access needs the Security privilege. In a session that does no
Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned nothing without `-Path` or for `-SecurityDescriptor`. When the computer of `-ServerName` couldn't be reached, the cmdlet warned that it had calculated the result on this computer, but returned no access instead of that result. Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned nothing without `-Path` or for `-SecurityDescriptor`. When the computer of `-ServerName` couldn't be reached, the cmdlet warned that it had calculated the result on this computer, but returned no access instead of that result.
Before 5.0.0-rc7, the warning about a computer that couldn't be reached didn't name the computer, and the cmdlet warned for every name of this computer except `localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name.
## RELATED LINKS ## RELATED LINKS
[Get-NTFSAccess](Get-NTFSAccess.md) [Get-NTFSAccess](Get-NTFSAccess.md)

8
Docs/Cmdlets/Get-NTFSOrphanedAudit.md

@ -163,7 +163,7 @@ You can pipe paths to this cmdlet, or objects that have a `Path` or `FullName` p
### Security2.FileSystemSecurity2[] ### Security2.FileSystemSecurity2[]
Security descriptors bind to the inherited `-SecurityDescriptor` parameter, but this cmdlet does not read their audit entries. Security descriptors that `Get-NTFSSecurityDescriptor` returned bind to `-SecurityDescriptor`, and the cmdlet examines their audit entries.
### Security2.IdentityReference2 ### Security2.IdentityReference2
@ -179,12 +179,14 @@ The cmdlet returns the audit entries whose account SID cannot be translated into
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. 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.
Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it the cmdlet reads the security descriptor without its SACL and reports no orphaned entries at all, which looks the same as a tree that has none. Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes the non-terminating error `ReadSecurityError` for each item, which reports "A required privilege is not held by the client", like `Get-NTFSAudit`.
If an item cannot be read, the cmdlet writes a warning and continues with the next item. Unlike `Get-NTFSAudit`, it does not try to take ownership of the item when access is denied. If the audit entries of an item can't be read, the cmdlet writes a `ReadSecurityError`, with the category `PermissionDenied` when access is denied, and continues with the next item; for a path that doesn't exist, it writes a `ReadError`. Like `Get-NTFSAudit`, it doesn't take ownership of the item, because ownership grants no access to the SACL.
Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection. Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection.
Before 5.0.0-rc7, the cmdlet read an item without its SACL when the Security privilege was missing and reported no orphaned entries, which looked the same as an item that has none. For an item that it couldn't read, it wrote a warning instead of an error.
## RELATED LINKS ## RELATED LINKS
[Get-NTFSAudit](Get-NTFSAudit.md) [Get-NTFSAudit](Get-NTFSAudit.md)

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. 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; paths that differ only in case name the same folder. 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. `-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, 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 parent folder whose path differed in case counted as not reported. 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 ## RELATED LINKS
[Get-NTFSAccess](Get-NTFSAccess.md) [Get-NTFSAccess](Get-NTFSAccess.md)

6
Docs/Cmdlets/Move-Item2.md

@ -24,7 +24,7 @@ The `Move-Item2` cmdlet moves the items in `-Path` to the location in `-Destinat
How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts. How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts.
Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder, or, with `-WhatIf`, a verbose message. Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; a file then moves with the `CopyAllowed` option, which allows it to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder, or, with `-WhatIf`, a verbose message.
The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`. The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`.
@ -190,10 +190,12 @@ With `-PassThru $true` the cmdlet returns a folder object for each folder that i
Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation. Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation.
The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. A folder can't move to another volume: the cmdlet writes a `MoveError` and leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead. The cmdlet chooses between two mutually exclusive move options for a file. Without `-Force` it moves a file with `CopyAllowed`, which permits the file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a file can't move to another volume when `-Force` is specified: the cmdlet writes the `MoveError` of Windows, "(17) The system cannot move the file to a different disk drive". A folder can't move to another volume at all: the cmdlet writes a `MoveError` with the category `InvalidOperation` that names the folder and the destination, and it leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead.
If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ([#21](https://github.com/raandree/NTFSSecurity/issues/21)). If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ([#21](https://github.com/raandree/NTFSSecurity/issues/21)).
Before 5.0.0-rc7, a folder moved with `CopyAllowed` as well, so a move to another volume copied and deleted it: an empty folder was deleted without being created at the destination, and a folder with files failed with an error that named one of its files.
## RELATED LINKS ## RELATED LINKS
[Copy-Item2](Copy-Item2.md) [Copy-Item2](Copy-Item2.md)

26
Docs/Cmdlets/New-NTFSHardLink.md

@ -14,15 +14,17 @@ Creates a hard link to an existing file.
## SYNTAX ## SYNTAX
``` ```
New-NTFSHardLink [[-Path] <String>] [[-Target] <String>] [-PassThru] [<CommonParameters>] New-NTFSHardLink [-Path] <String> [-Target] <String> [-PassThru] [<CommonParameters>]
``` ```
## DESCRIPTION ## DESCRIPTION
The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create *Path*, which points to *Target*". The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create *Path*, which points to *Target*". Both parameters are required.
The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location. The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location.
To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.
After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted. After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted.
By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created. By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created.
@ -61,6 +63,14 @@ PS C:\> Get-NTFSHardLink -Path C:\Data\Report-2026.txt | Select-Object -ExpandPr
This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns. This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns.
### Example 5: Create several links from a list
```PowerShell
PS C:\> Import-Csv -Path C:\Data\Links.csv | New-NTFSHardLink
```
This command creates one hard link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.
## PARAMETERS ## PARAMETERS
### -PassThru ### -PassThru
@ -88,7 +98,7 @@ Type: String
Parameter Sets: (All) Parameter Sets: (All)
Aliases: FullName Aliases: FullName
Required: False Required: True
Position: 1 Position: 1
Default value: None Default value: None
Accept pipeline input: True (ByPropertyName, ByValue) Accept pipeline input: True (ByPropertyName, ByValue)
@ -104,7 +114,7 @@ Type: String
Parameter Sets: (All) Parameter Sets: (All)
Aliases: Aliases:
Required: False Required: True
Position: 2 Position: 2
Default value: None Default value: None
Accept pipeline input: True (ByPropertyName, ByValue) Accept pipeline input: True (ByPropertyName, ByValue)
@ -120,6 +130,10 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
You can pass the path of the new link and the path of the target as strings. You can pass the path of the new link and the path of the target as strings.
### System.Management.Automation.PSObject
You can pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target, such as the rows of a CSV file that `Import-Csv` reads.
## OUTPUTS ## OUTPUTS
### Alphaleonis.Win32.Filesystem.FileInfo ### Alphaleonis.Win32.Filesystem.FileInfo
@ -136,12 +150,14 @@ Windows supports hard links only for files on the same NTFS volume. A link that
The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link. The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link.
The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged. The cmdlet does not overwrite anything. If `-Path` already exists, if `-Target` is missing or is a folder, or if a path contains a character that Windows doesn't allow, such as `|`, the cmdlet writes a non-terminating `CreateHardLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. It checks `-Path` before `-Target`, and the error for an existing `-Path`, a missing `-Target`, or a folder as `-Target` names that path. When Windows refuses the link, such as for a target on another volume, the cmdlet writes a `CreateHardLinkError` as well.
Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain. Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain.
Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause. Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause.
Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it used the current location, which is a folder. It stopped with a terminating error for an existing `-Path`, a missing `-Target`, a folder as `-Target`, or a link that Windows refused, in Windows PowerShell also for a path with a character that Windows doesn't allow, and every object piped to it failed with `GetDefaultValueFailed`.
## RELATED LINKS ## RELATED LINKS
[Get-NTFSHardLink](Get-NTFSHardLink.md) [Get-NTFSHardLink](Get-NTFSHardLink.md)

28
Docs/Cmdlets/New-NTFSSymbolicLink.md

@ -14,17 +14,19 @@ Creates a symbolic link to an existing file or folder.
## SYNTAX ## SYNTAX
``` ```
New-NTFSSymbolicLink [[-Path] <String>] [[-Target] <String>] [-PassThru] [<CommonParameters>] New-NTFSSymbolicLink [-Path] <String> [-Target] <String> [-PassThru] [<CommonParameters>]
``` ```
## DESCRIPTION ## DESCRIPTION
The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create *Path*, which points to *Target*". The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create *Path*, which points to *Target*". Both parameters are required.
The cmdlet inspects the target first and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item. Before it creates the link, the cmdlet inspects the target and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item.
Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path. Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path.
To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.
By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder. By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder.
## EXAMPLES ## EXAMPLES
@ -61,6 +63,14 @@ PS C:\> Test-Path2 -Path C:\Data\Current\Report.txt -PathType Leaf
This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder. This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder.
### Example 5: Create several links from a list
```PowerShell
PS C:\> Import-Csv -Path C:\Data\Links.csv | New-NTFSSymbolicLink
```
This command creates one symbolic link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.
## PARAMETERS ## PARAMETERS
### -PassThru ### -PassThru
@ -88,7 +98,7 @@ Type: String
Parameter Sets: (All) Parameter Sets: (All)
Aliases: FullName Aliases: FullName
Required: False Required: True
Position: 1 Position: 1
Default value: None Default value: None
Accept pipeline input: True (ByPropertyName, ByValue) Accept pipeline input: True (ByPropertyName, ByValue)
@ -104,7 +114,7 @@ Type: String
Parameter Sets: (All) Parameter Sets: (All)
Aliases: Aliases:
Required: False Required: True
Position: 2 Position: 2
Default value: None Default value: None
Accept pipeline input: True (ByPropertyName, ByValue) Accept pipeline input: True (ByPropertyName, ByValue)
@ -120,6 +130,10 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
You can pass the path of the new link and the path of the target as strings. You can pass the path of the new link and the path of the target as strings.
### System.Management.Automation.PSObject
You can pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target, such as the rows of a CSV file that `Import-Csv` reads.
## OUTPUTS ## OUTPUTS
### Alphaleonis.Win32.Filesystem.FileInfo ### Alphaleonis.Win32.Filesystem.FileInfo
@ -136,8 +150,12 @@ Creating a symbolic link on Windows requires the "Create symbolic links" user ri
Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving. Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving.
If `-Path` already exists, `-Target` is missing, or a path contains a character that Windows doesn't allow, such as `|`, the cmdlet writes a non-terminating `CreateSymbolicLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. It checks `-Path` before `-Target`, and the error for an existing `-Path` or a missing `-Target` names that path. When Windows refuses the link, such as with error 1314 without the right to create symbolic links, the cmdlet writes a `CreateSymbolicLinkError` as well.
Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected. Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected.
Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it created a link to the current folder. It stopped with a terminating error for an existing `-Path` or a link that Windows refused, in Windows PowerShell also for a path with a character that Windows doesn't allow, and every object piped to it failed with `GetDefaultValueFailed`. It checked `-Target` before `-Path`, and its errors for an existing `-Path` and a missing `-Target` named no path.
## RELATED LINKS ## RELATED LINKS
[New-NTFSHardLink](New-NTFSHardLink.md) [New-NTFSHardLink](New-NTFSHardLink.md)

18
Docs/FAQ.md

@ -75,3 +75,21 @@ relative path is resolved against the current file system location, also a
name that starts with a dot, such as `.gitignore`; before 5.0.0-rc6, the name that starts with a dot, such as `.gitignore`; before 5.0.0-rc6, the
cmdlets dropped the first two characters of such a name. See cmdlets dropped the first two characters of such a name. See
[Long paths](Concepts.md#long-paths). [Long paths](Concepts.md#long-paths).
## How do I compare the permissions of two items?
Compare the properties of the entries, not the entries themselves. Each
entry that `Get-NTFSAccess` returns is an object of its own, and like the
access rules of .NET, two entries are equal only when they are the same
object, even when they grant the same rights to the same account.
`Compare-Object` with `-Property` lists the entries that only one of the
items has:
```powershell
$properties = 'Account', 'AccessRights', 'AccessControlType', 'InheritanceFlags', 'PropagationFlags'
Compare-Object -ReferenceObject (Get-NTFSAccess -Path C:\Data\A) -DifferenceObject (Get-NTFSAccess -Path C:\Data\B) -Property $properties
```
The same works for the entries of `Get-NTFSAudit`, with `AuditFlags` in
place of `AccessControlType`. See
[Get-NTFSAccess](Cmdlets/Get-NTFSAccess.md).

7
NTFSSecurity/AccessCmdlets/GetEffectiveAccess.cs

@ -159,9 +159,10 @@ namespace NTFSSecurity
{ {
if (!result.FromRemote) if (!result.FromRemote)
{ {
WriteWarning("The effective rights can only be computed based on group membership on this" + // Since 5.0.0-rc7, the warning names the computer, which a command with many items can't tell otherwise.
" computer. For more accurate results, calculate effective access rights on " + WriteWarning(string.Format("The effective rights can only be computed based on group membership on this computer, " +
"the target computer"); "because the computer '{0}' can't be reached for a remote access check. " +
"For more accurate results, calculate effective access rights on that computer.", serverName));
} }
if (result.OperationFailed) if (result.OperationFailed)

12
NTFSSecurity/AuditCmdlets/Get-OrphanedAudit.cs

@ -46,13 +46,21 @@ namespace NTFSSecurity.AuditCmdlets
IEnumerable<FileSystemAuditRule2> acl = null; IEnumerable<FileSystemAuditRule2> acl = null;
// Only the SACL, like Get-NTFSAudit, which fails without the Security privilege. Before 5.0.0-rc7, the
// cmdlet read the item without its audit entries then and returned nothing, as for an item without
// orphaned entries, and it wrote a warning for an item that it couldn't read.
try try
{ {
acl = FileSystemAuditRule2.GetFileSystemAuditRules(item, !ExcludeExplicit, !ExcludeInherited, getInheritedFrom); acl = GetAuditRules(item);
}
catch (UnauthorizedAccessException ex)
{
this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.PermissionDenied, p));
continue;
} }
catch (Exception ex) catch (Exception ex)
{ {
this.WriteWarning(string.Format("Could not read item {0}. The error was: {1}", p, ex.Message)); this.WriteError(new ErrorRecord(ex, "ReadSecurityError", ErrorCategory.OpenError, p));
continue; continue;
} }

6
NTFSSecurity/AuditCmdlets/GetAudit.cs

@ -130,9 +130,11 @@ namespace NTFSSecurity
} }
} }
private IEnumerable<FileSystemAuditRule2> GetAuditRules(FileSystemInfo item) /// <summary>
/// Reads only the SACL of an item. Without the Security privilege, this fails instead of returning no entries.
/// </summary>
protected IEnumerable<FileSystemAuditRule2> GetAuditRules(FileSystemInfo item)
{ {
// Reading only the SACL fails without the Security privilege, instead of returning no entries.
var sd = new FileSystemSecurity2(item, System.Security.AccessControl.AccessControlSections.Audit); var sd = new FileSystemSecurity2(item, System.Security.AccessControl.AccessControlSections.Audit);
return FileSystemAuditRule2.GetFileSystemAuditRules(sd, !excludeExplicit, !excludeInherited, getInheritedFrom); return FileSystemAuditRule2.GetFileSystemAuditRules(sd, !excludeExplicit, !excludeInherited, getInheritedFrom);
} }

19
NTFSSecurity/ItemCmdlets/MoveItem2.cs

@ -130,13 +130,30 @@ namespace NTFSSecurity
} }
else else
{ {
((DirectoryInfo)item).MoveTo(actualDestination, force ? MoveOptions.ReplaceExisting : MoveOptions.CopyAllowed, PathFormat.RelativePath); // Without CopyAllowed, so that Windows refuses a move to another volume, which it can't do for a
// folder. Before 5.0.0-rc7, AlphaFS emulated such a move by copying and deleting: an empty folder
// was deleted without being created at the destination, and a folder with files failed with an
// error that named one of its files.
((DirectoryInfo)item).MoveTo(actualDestination, force ? MoveOptions.ReplaceExisting : MoveOptions.None, PathFormat.RelativePath);
WriteVerbose(string.Format("Directory '{0}' moved to '{1}'", resolvedPath, actualDestination)); WriteVerbose(string.Format("Directory '{0}' moved to '{1}'", resolvedPath, actualDestination));
} }
if (passThru) if (passThru)
WriteObject(item); WriteObject(item);
} }
catch (NotSameDeviceException ex)
{
// A file gets here only with -Force, which moves without CopyAllowed; it keeps the error of Windows.
if (item is DirectoryInfo)
{
var message = string.Format("The folder '{0}' can't move to another volume, '{1}'. Copy it with Copy-Item2, then remove it with Remove-Item2.", resolvedPath, actualDestination);
WriteError(new ErrorRecord(new System.IO.IOException(message, ex), "MoveError", ErrorCategory.InvalidOperation, resolvedPath));
}
else
{
WriteError(new ErrorRecord(ex, "MoveError", ErrorCategory.InvalidData, resolvedPath));
}
}
catch (System.IO.IOException ex) catch (System.IO.IOException ex)
{ {
WriteError(new ErrorRecord(ex, "MoveError", ErrorCategory.InvalidData, resolvedPath)); WriteError(new ErrorRecord(ex, "MoveError", ErrorCategory.InvalidData, resolvedPath));

121
NTFSSecurity/LinkCmdlets/NewHardLink.cs

@ -14,13 +14,17 @@ namespace NTFSSecurity
private bool passThru; private bool passThru;
System.Reflection.MethodInfo modeMethodInfo = null; System.Reflection.MethodInfo modeMethodInfo = null;
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] // Required since 5.0.0-rc7. Before, an omitted -Path failed with an index error, and an omitted -Target meant the
// current location, which is a folder.
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty] [ValidateNotNullOrEmpty]
[Alias("FullName")] [Alias("FullName")]
[FileSystemPathTransformation] [FileSystemPathTransformation]
public string Path public string Path
{ {
get { return paths[0]; } // PowerShell reads a parameter that takes pipeline input before it binds the input. Before 5.0.0-rc7, the
// empty list failed that read, so every piped object failed with GetDefaultValueFailed.
get { return paths.Count > 0 ? paths[0] : null; }
set set
{ {
paths.Clear(); paths.Clear();
@ -28,7 +32,7 @@ namespace NTFSSecurity
} }
} }
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty] [ValidateNotNullOrEmpty]
[FileSystemPathTransformation] [FileSystemPathTransformation]
public string Target public string Target
@ -53,59 +57,94 @@ namespace NTFSSecurity
protected override void ProcessRecord() protected override void ProcessRecord()
{ {
var path = paths[0]; string path;
string targetPath;
path = GetRelativePath(path); try
target = GetRelativePath(target); {
path = GetRelativePath(paths[0]);
targetPath = GetRelativePath(target);
}
// Windows PowerShell rejects a character that Windows doesn't allow in a path, such as |, already here.
catch (ArgumentException ex)
{
WriteError(new ErrorRecord(ex, "CreateHardLinkError", ErrorCategory.InvalidArgument, paths[0]));
return;
}
var root = System.IO.Path.GetPathRoot(path); var root = System.IO.Path.GetPathRoot(path);
try // Non-terminating errors, so that the links that follow in the pipeline are created as well. Before
// 5.0.0-rc7, an existing path, a missing target, or a folder as target stopped the pipeline.
FileSystemInfo temp = null;
if (TryGetFileSystemInfo2(path, out temp))
{ {
FileSystemInfo temp = null; var exists = new ArgumentException(string.Format("The file '{0}' does already exist, cannot create the link", path));
WriteError(new ErrorRecord(exists, "CreateHardLinkError", ErrorCategory.ResourceExists, path));
return;
}
if (TryGetFileSystemInfo2(path, out temp)) if (!TryGetFileSystemInfo2(targetPath, out temp))
throw new ArgumentException(string.Format("The file '{0}' does already exist, cannot create the link", path)); {
var missing = new System.IO.FileNotFoundException(string.Format("The target '{0}' does not exist, cannot create the link", targetPath), targetPath);
WriteError(new ErrorRecord(missing, "CreateHardLinkError", ErrorCategory.ObjectNotFound, path));
return;
}
if (!TryGetFileSystemInfo2(target, out temp)) if (temp is DirectoryInfo)
throw new ArgumentException(string.Format("The target '{0}' does not exist, cannot create the link", target)); {
else var folder = new ArgumentException(string.Format("The target '{0}' is not a file, cannot create the link", targetPath));
if (temp is DirectoryInfo) WriteError(new ErrorRecord(folder, "CreateHardLinkError", ErrorCategory.InvalidArgument, path));
throw new ArgumentException("The target is not a file, cannot create the link"); return;
}
try
{
File.CreateHardlink(path, targetPath);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "CreateHardLinkError", GetErrorCategory(ex), path));
return;
}
File.CreateHardlink(path, target); if (passThru)
{
IEnumerable<string> links;
try
{
links = File.EnumerateHardlinks(path).ToList();
}
// Windows can't list the names of a file on a network share: (50) The request is not supported.
// The link exists; before 5.0.0-rc6, this stopped the cmdlet with a terminating error.
catch (System.IO.IOException ex)
{
WriteError(new ErrorRecord(ex, "GetHardLinkError", ErrorCategory.ReadError, path));
return;
}
if (passThru) foreach (var link in links)
{ {
IEnumerable<string> links; var name = new PSObject(GetFileSystemInfo2(System.IO.Path.Combine(root, link.Substring(1))));
try name.Properties.Add(new PSCodeProperty("Mode", modeMethodInfo));
{ WriteObject(name);
links = File.EnumerateHardlinks(path).ToList();
}
// Windows can't list the names of a file on a network share: (50) The request is not supported.
// The link exists; before 5.0.0-rc6, this stopped the cmdlet with a terminating error.
catch (System.IO.IOException ex)
{
WriteError(new ErrorRecord(ex, "GetHardLinkError", ErrorCategory.ReadError, path));
return;
}
foreach (var link in links)
{
var target = new PSObject(GetFileSystemInfo2(System.IO.Path.Combine(root, link.Substring(1))));
target.Properties.Add(new PSCodeProperty("Mode", modeMethodInfo));
WriteObject(target);
}
} }
} }
catch (System.IO.FileNotFoundException ex)
{
WriteError(new ErrorRecord(ex, "CreateHardLinkError", ErrorCategory.WriteError, path));
}
} }
protected override void EndProcessing() protected override void EndProcessing()
{ {
base.EndProcessing(); base.EndProcessing();
} }
// In PowerShell 7, AlphaFS rejects a character that Windows doesn't allow in a path only when it creates the link.
internal static ErrorCategory GetErrorCategory(Exception ex)
{
if (ex is UnauthorizedAccessException)
return ErrorCategory.PermissionDenied;
if (ex is ArgumentException)
return ErrorCategory.InvalidArgument;
return ErrorCategory.WriteError;
}
} }
} }

76
NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs

@ -12,13 +12,17 @@ namespace NTFSSecurity
private bool passThru; private bool passThru;
System.Reflection.MethodInfo modeMethodInfo = null; System.Reflection.MethodInfo modeMethodInfo = null;
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] // Required since 5.0.0-rc7. Before, an omitted -Path failed with an index error, and an omitted -Target meant the
// current location, so the cmdlet created a link to the current folder.
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty] [ValidateNotNullOrEmpty]
[Alias("FullName")] [Alias("FullName")]
[FileSystemPathTransformation] [FileSystemPathTransformation]
public string Path public string Path
{ {
get { return paths[0]; } // PowerShell reads a parameter that takes pipeline input before it binds the input. Before 5.0.0-rc7, the
// empty list failed that read, so every piped object failed with GetDefaultValueFailed.
get { return paths.Count > 0 ? paths[0] : null; }
set set
{ {
paths.Clear(); paths.Clear();
@ -26,7 +30,7 @@ namespace NTFSSecurity
} }
} }
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] [Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty] [ValidateNotNullOrEmpty]
[FileSystemPathTransformation] [FileSystemPathTransformation]
public string Target public string Target
@ -51,36 +55,56 @@ namespace NTFSSecurity
protected override void ProcessRecord() protected override void ProcessRecord()
{ {
var path = paths[0]; string path;
string targetPath;
path = GetRelativePath(path);
target = GetRelativePath(target);
FileSystemInfo targetItem = null;
var root = System.IO.Path.GetPathRoot(path);
try try
{ {
targetItem = GetFileSystemInfo2(target); path = GetRelativePath(paths[0]);
targetPath = GetRelativePath(target);
}
// Windows PowerShell rejects a character that Windows doesn't allow in a path, such as |, already here.
catch (ArgumentException ex)
{
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", ErrorCategory.InvalidArgument, paths[0]));
return;
}
FileSystemInfo temp; // Non-terminating errors, so that the links that follow in the pipeline are created as well. Before
if (TryGetFileSystemInfo2(path, out temp)) // 5.0.0-rc7, an existing path and a failure to create the link stopped the pipeline, the cmdlet checked
{ // the target first, and its errors named neither path.
throw new ArgumentException("The path does already exist, cannot create link"); FileSystemInfo temp;
} if (TryGetFileSystemInfo2(path, out temp))
{
var exists = new ArgumentException(string.Format("The path '{0}' does already exist, cannot create the link", path));
WriteError(new ErrorRecord(exists, "CreateSymbolicLinkError", ErrorCategory.ResourceExists, path));
return;
}
File.CreateSymbolicLink(path, target, targetItem is FileInfo ? SymbolicLinkTarget.File : SymbolicLinkTarget.Directory); FileSystemInfo targetItem;
if (!TryGetFileSystemInfo2(targetPath, out targetItem))
{
var missing = new System.IO.FileNotFoundException(string.Format("The target '{0}' does not exist, cannot create the link", targetPath), targetPath);
WriteError(new ErrorRecord(missing, "CreateSymbolicLinkError", ErrorCategory.ObjectNotFound, path));
return;
}
if (passThru) try
{ {
if (targetItem is FileInfo) File.CreateSymbolicLink(path, targetPath, targetItem is FileInfo ? SymbolicLinkTarget.File : SymbolicLinkTarget.Directory);
WriteObject(new FileInfo(path));
else
WriteObject(new DirectoryInfo(path));
}
} }
catch (System.IO.FileNotFoundException ex) // Without the right to create symbolic links: (1314) A required privilege is not held by the client.
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", NewHardLink.GetErrorCategory(ex), path));
return;
}
if (passThru)
{ {
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", ErrorCategory.ObjectNotFound, path)); if (targetItem is FileInfo)
WriteObject(new FileInfo(path));
else
WriteObject(new DirectoryInfo(path));
} }
} }

2
NTFSSecurity/NTFSSecurity.psd1

@ -102,7 +102,7 @@
ProjectUri = 'https://github.com/raandree/NTFSSecurity' ProjectUri = 'https://github.com/raandree/NTFSSecurity'
ReleaseNotes = 'https://github.com/raandree/NTFSSecurity/blob/master/CHANGELOG.md' ReleaseNotes = 'https://github.com/raandree/NTFSSecurity/blob/master/CHANGELOG.md'
# Remove the prerelease label for the final release, see Docs/Contributing/05-Releasing.md # Remove the prerelease label for the final release, see Docs/Contributing/05-Releasing.md
Prerelease = 'rc6' Prerelease = 'rc7'
} }
} }
} }

53
NTFSSecurity/SimpleAccessCmdlets/SimpleAccessCmdlets.cs

@ -25,9 +25,9 @@ namespace NTFSSecurity
set { includeRootFolder = value; } 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; DirectoryInfo item;
FileSystemInfo previousItem;
bool isFirstFolder = true; bool isFirstFolder = true;
protected override void ProcessRecord() protected override void ProcessRecord()
@ -73,16 +73,21 @@ namespace NTFSSecurity
var acl = FilterAccount(FileSystemAccessRule2.GetFileSystemAccessRules(item, !ExcludeExplicit, !ExcludeInherited).Select(ace => ace.ToSimpleFileSystemAccessRule2())).ToList(); var acl = FilterAccount(FileSystemAccessRule2.GetFileSystemAccessRules(item, !ExcludeExplicit, !ExcludeInherited).Select(ace => ace.ToSimpleFileSystemAccessRule2())).ToList();
FileSystemInfo parent = null;
try try
{ {
previousItem = item.GetParent(); parent = item.GetParent();
} }
catch { } 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); aceList.AddRange(acl);
acl.ForEach(ace => WriteObject(ace)); acl.ForEach(ace => WriteObject(ace));
@ -90,32 +95,16 @@ namespace NTFSSecurity
} }
else else
{ {
if (previousAcls.ContainsKey(previousItem.FullName)) previousAcls[item.FullName] = acl;
{
previousAcl = previousAcls[previousItem.FullName]; // The entries that the parent folder doesn't cover for the same account and access type
previousAcls.Add(item.FullName, acl); var diffAcl = acl.Where(ace => !previousAcl.Any(prevAce =>
prevAce.AccessControlType == ace.AccessControlType &
List<SimpleFileSystemAccessRule> diffAcl = new List<SimpleFileSystemAccessRule>(); (prevAce.AccessRights & ace.AccessRights) == ace.AccessRights &
prevAce.Identity == ace.Identity)).ToList();
foreach (var ace in acl)
{ aceList.AddRange(diffAcl);
var equalsUser = previousAcl.Where(prevAce => prevAce.Identity == ace.Identity); diffAcl.ForEach(ace => WriteObject(ace));
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));
}
} }
} }

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

@ -4945,7 +4945,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:description> <maml:description>
<maml:para>Calculates the rights an account really has on a file or a folder and writes the result as a single `Security2.FileSystemAccessRule2` object per item. The cmdlet evaluates the complete discretionary access control list (DACL) of the item against the group memberships of the account with the Windows Authorization API, so allow entries, deny entries, and inherited entries are combined the same way the Windows access check combines them. This is the equivalent of the "Effective Access" tab of the advanced security dialog.</maml:para> <maml:para>Calculates the rights an account really has on a file or a folder and writes the result as a single `Security2.FileSystemAccessRule2` object per item. The cmdlet evaluates the complete discretionary access control list (DACL) of the item against the group memberships of the account with the Windows Authorization API, so allow entries, deny entries, and inherited entries are combined the same way the Windows access check combines them. This is the equivalent of the "Effective Access" tab of the advanced security dialog.</maml:para>
<maml:para>The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports.</maml:para> <maml:para>The calculation covers the NTFS permissions of the item only. Share permissions are stored in a separate security descriptor and are not part of the result, so access over a network share can be more restrictive than this cmdlet reports.</maml:para>
<maml:para>When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.</maml:para> <maml:para>When `-Account` is omitted, the account that runs the session is used. `-ServerName` selects the computer whose authorization manager resolves the group memberships of the account and defaults to `localhost`; when the remote authorization manager of the named computer cannot be reached, the cmdlet falls back to the local one and warns that the result is based on the group memberships known on this computer and may be inaccurate. The warning names the computer that couldn't be reached. For a name of this computer, such as `localhost`, `.`, or its computer name, the local authorization manager gives the result of the named computer, so the cmdlet doesn't warn. The authorization manager of the named computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes an error and doesn't fall back. Reading effective access relies on the Security privilege, and the cmdlet warns when the account does not hold it or the privilege is disabled.</maml:para>
<maml:para>When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again.</maml:para> <maml:para>When `-Path` is omitted, the cmdlet calculates the effective access to the current location. In the `SecurityDescriptor` parameter set, it calculates the effective access from a `Security2.FileSystemSecurity2` object that `Get-NTFSSecurityDescriptor` returned, without reading the item again.</maml:para>
</maml:description> </maml:description>
<command:syntax> <command:syntax>
@ -5155,6 +5155,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</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>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>Reading effective access needs the Security privilege. In a session that does not hold it, the cmdlet warns before it starts and the calculation may fail with an error. Use `Enable-Privileges` in an elevated session to enable the privilege, and `Get-Privileges` to see which privileges the session holds. When the calculation fails, the error names the cause that Windows reported, such as a security descriptor without an owner; before 5.0.0, it blamed a missing Security privilege whenever the privilege wasn't enabled.</maml:para> <maml:para>Reading effective access needs the Security privilege. In a session that does not hold it, the cmdlet warns before it starts and the calculation may fail with an error. Use `Enable-Privileges` in an elevated session to enable the privilege, and `Get-Privileges` to see which privileges the session holds. When the calculation fails, the error names the cause that Windows reported, such as a security descriptor without an owner; before 5.0.0, it blamed a missing Security privilege whenever the privilege wasn't enabled.</maml:para>
<maml:para>Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned nothing without `-Path` or for `-SecurityDescriptor`. When the computer of `-ServerName` couldn't be reached, the cmdlet warned that it had calculated the result on this computer, but returned no access instead of that result.</maml:para> <maml:para>Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned nothing without `-Path` or for `-SecurityDescriptor`. When the computer of `-ServerName` couldn't be reached, the cmdlet warned that it had calculated the result on this computer, but returned no access instead of that result.</maml:para>
<maml:para>Before 5.0.0-rc7, the warning about a computer that couldn't be reached didn't name the computer, and the cmdlet warned for every name of this computer except `localhost` in lowercase, such as `.`, `LOCALHOST`, or the computer name.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -5988,7 +5989,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:name>Security2.FileSystemSecurity2[]</maml:name> <maml:name>Security2.FileSystemSecurity2[]</maml:name>
</dev:type> </dev:type>
<maml:description> <maml:description>
<maml:para>Security descriptors bind to the inherited `-SecurityDescriptor` parameter, but this cmdlet does not read their audit entries.</maml:para> <maml:para>Security descriptors that `Get-NTFSSecurityDescriptor` returned bind to `-SecurityDescriptor`, and the cmdlet examines their audit entries.</maml:para>
</maml:description> </maml:description>
</command:inputType> </command:inputType>
<command:inputType> <command:inputType>
@ -6013,9 +6014,10 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<maml:alertSet> <maml:alertSet>
<maml:alert> <maml:alert>
<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>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>Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it the cmdlet reads the security descriptor without its SACL and reports no orphaned entries at all, which looks the same as a tree that has none.</maml:para> <maml:para>Reading the SACL requires the Security privilege (`SeSecurityPrivilege`, "Manage auditing and security log"), so run this cmdlet in an elevated session of an account that holds that privilege. Without it, the cmdlet writes the non-terminating error `ReadSecurityError` for each item, which reports "A required privilege is not held by the client", like `Get-NTFSAudit`.</maml:para>
<maml:para>If an item cannot be read, the cmdlet writes a warning and continues with the next item. Unlike `Get-NTFSAudit`, it does not try to take ownership of the item when access is denied.</maml:para> <maml:para>If the audit entries of an item can't be read, the cmdlet writes a `ReadSecurityError`, with the category `PermissionDenied` when access is denied, and continues with the next item; for a path that doesn't exist, it writes a `ReadError`. Like `Get-NTFSAudit`, it doesn't take ownership of the item, because ownership grants no access to the SACL.</maml:para>
<maml:para>Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection.</maml:para> <maml:para>Before 5.0.0, the cmdlet ignored `-Account` and `-SecurityDescriptor` and wrote the entries of each item as one collection.</maml:para>
<maml:para>Before 5.0.0-rc7, the cmdlet read an item without its SACL when the Security privilege was missing and reported no orphaned entries, which looked the same as an item that has none. For an item that it couldn't read, it wrote a warning instead of an error.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -6385,7 +6387,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</command:details> </command:details>
<maml:description> <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>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; paths that differ only in case name the same folder. 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>`-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: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> </maml:description>
@ -6628,6 +6630,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>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>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, 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 parent folder whose path differed in case counted as not reported. 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:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -6787,7 +6790,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:description> <maml:description>
<maml:para>The `Move-Item2` cmdlet moves the items in `-Path` to the location in `-Destination`. It is the long-path counterpart of the built-in `Move-Item` cmdlet: it works through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), so source and destination may be longer than the 260-character `MAX_PATH` limit. Files and folders can both be moved, and a folder is moved with everything it contains.</maml:para> <maml:para>The `Move-Item2` cmdlet moves the items in `-Path` to the location in `-Destination`. It is the long-path counterpart of the built-in `Move-Item` cmdlet: it works through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), so source and destination may be longer than the 260-character `MAX_PATH` limit. Files and folders can both be moved, and a folder is moved with everything it contains.</maml:para>
<maml:para>How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts.</maml:para> <maml:para>How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts.</maml:para>
<maml:para>Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder, or, with `-WhatIf`, a verbose message.</maml:para> <maml:para>Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; a file then moves with the `CopyAllowed` option, which allows it to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder, or, with `-WhatIf`, a verbose message.</maml:para>
<maml:para>The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`.</maml:para> <maml:para>The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`.</maml:para>
</maml:description> </maml:description>
<command:syntax> <command:syntax>
@ -6978,8 +6981,9 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert> <maml:alert>
<maml:para>`Move-Item2` moves through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), which is why it handles source and destination paths that exceed the 260-character `MAX_PATH` limit of the built-in `Move-Item` cmdlet.</maml:para> <maml:para>`Move-Item2` moves through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), which is why it handles source and destination paths that exceed the 260-character `MAX_PATH` limit of the built-in `Move-Item` cmdlet.</maml:para>
<maml:para>Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation.</maml:para> <maml:para>Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation.</maml:para>
<maml:para>The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. A folder can't move to another volume: the cmdlet writes a `MoveError` and leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead.</maml:para> <maml:para>The cmdlet chooses between two mutually exclusive move options for a file. Without `-Force` it moves a file with `CopyAllowed`, which permits the file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a file can't move to another volume when `-Force` is specified: the cmdlet writes the `MoveError` of Windows, "(17) The system cannot move the file to a different disk drive". A folder can't move to another volume at all: the cmdlet writes a `MoveError` with the category `InvalidOperation` that names the folder and the destination, and it leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead.</maml:para>
<maml:para>If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ( #21 (https://github.com/raandree/NTFSSecurity/issues/21)).</maml:para> <maml:para>If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ( #21 (https://github.com/raandree/NTFSSecurity/issues/21)).</maml:para>
<maml:para>Before 5.0.0-rc7, a folder moved with `CopyAllowed` as well, so a move to another volume copied and deleted it: an empty folder was deleted without being created at the destination, and a folder with files failed with an error that named one of its files.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -7049,15 +7053,16 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</maml:description> </maml:description>
</command:details> </command:details>
<maml:description> <maml:description>
<maml:para>The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create Path , which points to Target ".</maml:para> <maml:para>The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create Path , which points to Target ". Both parameters are required.</maml:para>
<maml:para>The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location.</maml:para> <maml:para>The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location.</maml:para>
<maml:para>To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.</maml:para>
<maml:para>After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted.</maml:para> <maml:para>After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted.</maml:para>
<maml:para>By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created.</maml:para> <maml:para>By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created.</maml:para>
</maml:description> </maml:description>
<command:syntax> <command:syntax>
<command:syntaxItem> <command:syntaxItem>
<maml:name>New-NTFSHardLink</maml:name> <maml:name>New-NTFSHardLink</maml:name>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name> <maml:name>Path</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para> <maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para>
@ -7069,7 +7074,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>None</dev:defaultValue> <dev:defaultValue>None</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name> <maml:name>Target</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para> <maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para>
@ -7107,7 +7112,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>False</dev:defaultValue> <dev:defaultValue>False</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name> <maml:name>Path</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para> <maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para>
@ -7119,7 +7124,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>None</dev:defaultValue> <dev:defaultValue>None</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name> <maml:name>Target</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para> <maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para>
@ -7141,6 +7146,14 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para> <maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para>
</maml:description> </maml:description>
</command:inputType> </command:inputType>
<command:inputType>
<dev:type>
<maml:name>System.Management.Automation.PSObject</maml:name>
</dev:type>
<maml:description>
<maml:para>You can pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target, such as the rows of a CSV file that `Import-Csv` reads.</maml:para>
</maml:description>
</command:inputType>
</command:inputTypes> </command:inputTypes>
<command:returnValues> <command:returnValues>
<command:returnValue> <command:returnValue>
@ -7164,9 +7177,10 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert> <maml:alert>
<maml:para>Windows supports hard links only for files on the same NTFS volume. A link that points to a file on another volume, or a target on a file system that does not implement hard links, cannot be created.</maml:para> <maml:para>Windows supports hard links only for files on the same NTFS volume. A link that points to a file on another volume, or a target on a file system that does not implement hard links, cannot be created.</maml:para>
<maml:para>The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link.</maml:para> <maml:para>The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link.</maml:para>
<maml:para>The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged.</maml:para> <maml:para>The cmdlet does not overwrite anything. If `-Path` already exists, if `-Target` is missing or is a folder, or if a path contains a character that Windows doesn't allow, such as `|`, the cmdlet writes a non-terminating `CreateHardLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. It checks `-Path` before `-Target`, and the error for an existing `-Path`, a missing `-Target`, or a folder as `-Target` names that path. When Windows refuses the link, such as for a target on another volume, the cmdlet writes a `CreateHardLinkError` as well.</maml:para>
<maml:para>Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain.</maml:para> <maml:para>Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain.</maml:para>
<maml:para>Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause.</maml:para> <maml:para>Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause.</maml:para>
<maml:para>Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it used the current location, which is a folder. It stopped with a terminating error for an existing `-Path`, a missing `-Target`, a folder as `-Target`, or a link that Windows refused, in Windows PowerShell also for a path with a character that Windows doesn't allow, and every object piped to it failed with `GetDefaultValueFailed`.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -7198,6 +7212,13 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns.</maml:para> <maml:para>This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns.</maml:para>
</dev:remarks> </dev:remarks>
</command:example> </command:example>
<command:example>
<maml:title>--------- Example 5: Create several links from a list ---------</maml:title>
<dev:code>PS C:\&gt; Import-Csv -Path C:\Data\Links.csv | New-NTFSHardLink</dev:code>
<dev:remarks>
<maml:para>This command creates one hard link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.</maml:para>
</dev:remarks>
</command:example>
</command:examples> </command:examples>
<command:relatedLinks> <command:relatedLinks>
<maml:navigationLink> <maml:navigationLink>
@ -7232,15 +7253,16 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</maml:description> </maml:description>
</command:details> </command:details>
<maml:description> <maml:description>
<maml:para>The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create Path , which points to Target ".</maml:para> <maml:para>The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create Path , which points to Target ". Both parameters are required.</maml:para>
<maml:para>The cmdlet inspects the target first and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item.</maml:para> <maml:para>Before it creates the link, the cmdlet inspects the target and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item.</maml:para>
<maml:para>Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path.</maml:para> <maml:para>Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path.</maml:para>
<maml:para>To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.</maml:para>
<maml:para>By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder.</maml:para> <maml:para>By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder.</maml:para>
</maml:description> </maml:description>
<command:syntax> <command:syntax>
<command:syntaxItem> <command:syntaxItem>
<maml:name>New-NTFSSymbolicLink</maml:name> <maml:name>New-NTFSSymbolicLink</maml:name>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name> <maml:name>Path</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para> <maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para>
@ -7252,7 +7274,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>None</dev:defaultValue> <dev:defaultValue>None</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name> <maml:name>Target</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para> <maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para>
@ -7290,7 +7312,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>False</dev:defaultValue> <dev:defaultValue>False</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name> <maml:name>Path</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para> <maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para>
@ -7302,7 +7324,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type> </dev:type>
<dev:defaultValue>None</dev:defaultValue> <dev:defaultValue>None</dev:defaultValue>
</command:parameter> </command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none"> <command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name> <maml:name>Target</maml:name>
<maml:description> <maml:description>
<maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para> <maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para>
@ -7324,6 +7346,14 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para> <maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para>
</maml:description> </maml:description>
</command:inputType> </command:inputType>
<command:inputType>
<dev:type>
<maml:name>System.Management.Automation.PSObject</maml:name>
</dev:type>
<maml:description>
<maml:para>You can pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target, such as the rows of a CSV file that `Import-Csv` reads.</maml:para>
</maml:description>
</command:inputType>
</command:inputTypes> </command:inputTypes>
<command:returnValues> <command:returnValues>
<command:returnValue> <command:returnValue>
@ -7347,7 +7377,9 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert> <maml:alert>
<maml:para>Creating a symbolic link on Windows requires the "Create symbolic links" user right, `SeCreateSymbolicLinkPrivilege`, which is granted to the Administrators group by default. Without that right, Windows rejects the operation with error 1314, "A required privilege is not held by the client", so run the cmdlet from an elevated session or grant the right to the account. Windows Developer Mode doesn't change this: it lets accounts without that right create symbolic links only in programs that request it, such as `mklink`, and the cmdlet doesn't.</maml:para> <maml:para>Creating a symbolic link on Windows requires the "Create symbolic links" user right, `SeCreateSymbolicLinkPrivilege`, which is granted to the Administrators group by default. Without that right, Windows rejects the operation with error 1314, "A required privilege is not held by the client", so run the cmdlet from an elevated session or grant the right to the account. Windows Developer Mode doesn't change this: it lets accounts without that right create symbolic links only in programs that request it, such as `mklink`, and the cmdlet doesn't.</maml:para>
<maml:para>Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving.</maml:para> <maml:para>Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving.</maml:para>
<maml:para>If `-Path` already exists, `-Target` is missing, or a path contains a character that Windows doesn't allow, such as `|`, the cmdlet writes a non-terminating `CreateSymbolicLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. It checks `-Path` before `-Target`, and the error for an existing `-Path` or a missing `-Target` names that path. When Windows refuses the link, such as with error 1314 without the right to create symbolic links, the cmdlet writes a `CreateSymbolicLinkError` as well.</maml:para>
<maml:para>Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected.</maml:para> <maml:para>Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected.</maml:para>
<maml:para>Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it created a link to the current folder. It stopped with a terminating error for an existing `-Path` or a link that Windows refused, in Windows PowerShell also for a path with a character that Windows doesn't allow, and every object piped to it failed with `GetDefaultValueFailed`. It checked `-Target` before `-Path`, and its errors for an existing `-Path` and a missing `-Target` named no path.</maml:para>
</maml:alert> </maml:alert>
</maml:alertSet> </maml:alertSet>
<command:examples> <command:examples>
@ -7379,6 +7411,13 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder.</maml:para> <maml:para>This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder.</maml:para>
</dev:remarks> </dev:remarks>
</command:example> </command:example>
<command:example>
<maml:title>--------- Example 5: Create several links from a list ---------</maml:title>
<dev:code>PS C:\&gt; Import-Csv -Path C:\Data\Links.csv | New-NTFSSymbolicLink</dev:code>
<dev:remarks>
<maml:para>This command creates one symbolic link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.</maml:para>
</dev:remarks>
</command:example>
</command:examples> </command:examples>
<command:relatedLinks> <command:relatedLinks>
<maml:navigationLink> <maml:navigationLink>

35
Security2/Win32/Lib.cs

@ -134,6 +134,36 @@ namespace Security2
} }
#region Win32 Wrapper #region Win32 Wrapper
// Whether a name of -ServerName names this computer: localhost in any case, ., the NetBIOS name, the DNS host
// name, or the fully qualified domain name. It must not throw, because GetEffectiveAccess hides every exception
// of the resource manager behind a result without access.
private static bool IsLocalComputer(string serverName)
{
if (string.IsNullOrEmpty(serverName))
{
return false;
}
if (serverName == "." ||
string.Equals(serverName, "localhost", StringComparison.OrdinalIgnoreCase) ||
string.Equals(serverName, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
try
{
var properties = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties();
return string.Equals(serverName, properties.HostName, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(properties.DomainName) &&
string.Equals(serverName, properties.HostName + "." + properties.DomainName, StringComparison.OrdinalIgnoreCase));
}
catch (System.Net.NetworkInformation.NetworkInformationException)
{
return false;
}
}
private void GetEffectivePermissions_AuthzInitializeResourceManager(string serverName, out bool remoteServerAvailable) private void GetEffectivePermissions_AuthzInitializeResourceManager(string serverName, out bool remoteServerAvailable)
{ {
remoteServerAvailable = false; remoteServerAvailable = false;
@ -157,7 +187,10 @@ namespace Security2
throw new Win32Exception(error); throw new Win32Exception(error);
} }
if (serverName == "localhost") // The local authorization manager is the one of this computer, so its result is accurate for any name
// of this computer. Before 5.0.0-rc7, only localhost in lowercase counted, and the cmdlet warned for
// the others, such as ., the computer name, or LOCALHOST.
if (IsLocalComputer(serverName))
{ {
remoteServerAvailable = true; remoteServerAvailable = true;
} }

76
Tests/Access.Tests.ps1

@ -13,6 +13,11 @@ BeforeDiscovery {
# Assigning an owner other than the user or one of its groups needs the Restore privilege. # Assigning an owner other than the user or one of its groups needs the Restore privilege.
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege' $canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' $holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
# Names of this computer for -ServerName of Get-NTFSEffectiveAccess: its NetBIOS name, its DNS host name, and, in a
# domain, its fully qualified name
$ipProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
$localComputerNames = @(@('LOCALHOST', '.', $env:COMPUTERNAME, $ipProperties.HostName) +
@(if ($ipProperties.DomainName) { '{0}.{1}' -f $ipProperties.HostName, $ipProperties.DomainName }) | Sort-Object -Unique)
} }
BeforeAll { BeforeAll {
@ -145,8 +150,27 @@ Describe 'Get-NTFSEffectiveAccess' {
$accessErrors | Should -BeNullOrEmpty $accessErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1 $result | Should -HaveCount 1
$result[0].AccessRights | Should -Be $expected.AccessRights $result[0].AccessRights | Should -Be $expected.AccessRights
$accessWarnings.Message | Should -Contain ('The effective rights can only be computed based on group membership on this computer. ' + # Before 5.0.0-rc7, the warning didn't name the computer.
'For more accurate results, calculate effective access rights on the target computer') $accessWarnings.Message | Should -Contain ("The effective rights can only be computed based on group membership on this computer, " +
"because the computer 'ntfssecurity-test.invalid' can't be reached for a remote access check. " +
'For more accurate results, calculate effective access rights on that computer.')
}
}
# Not every computer offers the remote interface of the authorization manager; the cmdlet then calculates the result
# with the local one, which for a name of this computer is the result of that computer. Before 5.0.0-rc7, the cmdlet
# warned that the computer couldn't be reached for every name of this computer but localhost in lowercase.
Context 'When -ServerName names this computer' {
It 'Should return the result of localhost for <_> and warn no more than for localhost' -ForEach $localComputerNames {
$expected = Get-NTFSEffectiveAccess -Path $effectiveFile -WarningVariable expectedWarnings -WarningAction SilentlyContinue -ErrorAction Stop
$result = @(Get-NTFSEffectiveAccess -Path $effectiveFile -ServerName $_ -WarningVariable accessWarnings -WarningAction SilentlyContinue -ErrorVariable accessErrors -ErrorAction SilentlyContinue)
$accessErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
$result[0].AccessRights | Should -Be $expected.AccessRights
# Without the Security privilege, the cmdlet warns about it for every name.
@($accessWarnings.Message) -join '|' | Should -Be (@($expectedWarnings.Message) -join '|')
} }
} }
} }
@ -326,6 +350,54 @@ Describe 'Get-NTFSSimpleAccess' {
@($result | Where-Object -Property FullName -EQ -Value $child).Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101' @($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. The test reads the
# entries of the drive root and changes nothing there.
It 'Should report all entries of a drive root, which has no parent folder' {
$root = [IO.Path]::GetPathRoot($child)
$rootAlone = @(Get-NTFSSimpleAccess -Path $root -IncludeRootFolder:$false -ErrorAction Stop)
$rootAlone | Should -Not -BeNullOrEmpty
$result = @(Get-NTFSSimpleAccess -Path $child, $root -IncludeRootFolder:$false -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
$simpleErrors | Should -BeNullOrEmpty
@($result | Where-Object -Property FullName -EQ -Value $root) | Should -HaveCount $rootAlone.Count
}
# Windows doesn't distinguish paths by case. Before 5.0.0-rc7, the cmdlet didn't recognize the parent folder of a
# folder whose path differed from it in case, and left the folder out.
It 'Should compare a folder with its parent folder also when their paths differ in case' {
$result = @(Get-NTFSSimpleAccess -Path $parent, $child.ToUpperInvariant() -IncludeRootFolder:$false -ErrorAction Stop)
$childEntries = @($result | Where-Object -Property FullName -EQ -Value $child)
$childEntries | Should -HaveCount 1
$childEntries[0].Identity.Sid | Should -Be 'S-1-5-21-1-2-3-3101'
}
It 'Should report the parent folder of the first path first by default' { It 'Should report the parent folder of the first path first by default' {
$result = @(Get-NTFSSimpleAccess -Path $child -ErrorAction Stop) $result = @(Get-NTFSSimpleAccess -Path $child -ErrorAction Stop)

24
Tests/Audit.Tests.ps1

@ -242,7 +242,7 @@ Describe 'Get-NTFSOrphanedAudit' {
} }
} }
It 'Should write an error for a path that does not exist and continue with the next path' { It 'Should write an error for a path that does not exist and continue with the next path' -Skip:(-not $canReadAudit) {
$result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) $result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
$orphanedErrors | Should -HaveCount 1 $orphanedErrors | Should -HaveCount 1
@ -251,6 +251,19 @@ Describe 'Get-NTFSOrphanedAudit' {
$result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile } $result | ForEach-Object -Process { $_.FullName | Should -Be $orphanedFile }
} }
# Since 5.0.0-rc7, the next path has an error of its own without the Security privilege, which shows that the cmdlet
# continued with it.
It 'Should write an error for a path that does not exist and continue with the next path without the Security privilege' -Skip:$canReadAudit {
$result = @(Get-NTFSOrphanedAudit -Path $missing, $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
$result | Should -BeNullOrEmpty
$orphanedErrors | Should -HaveCount 2
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadError,*'
$orphanedErrors[0].TargetObject | Should -Be $missing
$orphanedErrors[1].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$orphanedErrors[1].TargetObject | Should -Be $orphanedFile
}
It 'Should write an error for a security descriptor that was read without the audit entries' { It 'Should write an error for a security descriptor that was read without the audit entries' {
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList ( $sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
(Get-Item2 -Path $orphanedFile), [System.Security.AccessControl.AccessControlSections]::Access (Get-Item2 -Path $orphanedFile), [System.Security.AccessControl.AccessControlSections]::Access
@ -263,12 +276,15 @@ Describe 'Get-NTFSOrphanedAudit' {
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*' $orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
} }
# The cmdlet page: without the privilege, the cmdlet reads no audit entries and reports none. # Before 5.0.0-rc7, the cmdlet read the item without its audit entries and returned nothing, as for an item without
It 'Should return nothing and write no error without the Security privilege' -Skip:$canReadAudit { # orphaned entries; it now writes the error of Get-NTFSAudit.
It 'Should write a ReadSecurityError without the Security privilege instead of returning nothing' -Skip:$canReadAudit {
$result = @(Get-NTFSOrphanedAudit -Path $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue) $result = @(Get-NTFSOrphanedAudit -Path $orphanedFile -ErrorVariable orphanedErrors -ErrorAction SilentlyContinue)
$orphanedErrors | Should -BeNullOrEmpty
$result | Should -BeNullOrEmpty $result | Should -BeNullOrEmpty
$orphanedErrors | Should -HaveCount 1
$orphanedErrors[0].FullyQualifiedErrorId | Should -BeLike 'ReadSecurityError,*'
$orphanedErrors[0].TargetObject | Should -Be $orphanedFile
} }
} }

63
Tests/ItemCmdlets.Tests.ps1

@ -6,6 +6,13 @@
)] )]
param () param ()
BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
# A path on the administrative share of the drive of the sandboxes is another volume for Windows, like a share of a
# file server.
$canUseAdminShare = Test-AdminShareAvailable
}
BeforeAll { BeforeAll {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1' $modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
@ -349,6 +356,62 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' {
} }
} }
Describe 'Move-Item2' {
# Windows can't move a folder to another volume. Before 5.0.0-rc7, the cmdlet let AlphaFS emulate the move by
# copying and deleting, which failed for a folder with files with an error that named a file of the source, and
# which deleted an empty folder without creating it at the destination.
It 'Should refuse to move <Kind> folder to another volume and leave it in place' -Skip:(-not $canUseAdminShare) -ForEach @(
@{ Kind = 'an empty' }
@{ Kind = 'a non-empty' }
) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolume' -Directory
if ($Kind -eq 'a non-empty') {
Set-Content -LiteralPath (Join-Path -Path $source -ChildPath 'File.txt') -Value 'File'
}
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -ErrorVariable moveErrors -ErrorAction SilentlyContinue
$moveErrors | Should -HaveCount 1
$moveErrors[0].FullyQualifiedErrorId | Should -BeLike 'MoveError,*'
$moveErrors[0].CategoryInfo.Category | Should -Be 'InvalidOperation'
$moveErrors[0].Exception.Message | Should -BeLike "*'$source'*another volume*"
$moveErrors[0].TargetObject | Should -Be $source
$source | Should -Exist
$destination | Should -Not -Exist
}
It 'Should still move a file to another volume' -Skip:(-not $canUseAdminShare) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolumeFile'
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -ErrorAction Stop
$source | Should -Not -Exist
Get-Content -LiteralPath $destination | Should -Be 'CrossVolumeFile'
}
# With -Force, a file moves without CopyAllowed, which Windows refuses for another volume, as the cmdlet page says.
# The cmdlet writes that error of Windows, (17) "The system cannot move the file to a different disk drive", and
# not the error for a folder.
It 'Should write the error of Windows for a file that it moves with -Force to another volume' -Skip:(-not $canUseAdminShare) {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'CrossVolumeForce'
$destination = Join-Path -Path $sandbox -ChildPath ('Moved-{0}' -f (Split-Path -Path $source -Leaf))
Assert-TestSandboxPath -Sandbox $sandbox -Path $destination
Move-Item2 -Path $source -Destination (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $destination) -Force -ErrorVariable moveErrors -ErrorAction SilentlyContinue
$moveErrors | Should -HaveCount 1
$moveErrors[0].FullyQualifiedErrorId | Should -BeLike 'MoveError,*'
$moveErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
'0x{0:X8}' -f $moveErrors[0].Exception.HResult | Should -Be '0x80070011'
$source | Should -Exist
$destination | Should -Not -Exist
}
}
Describe 'Copy-Item2' { Describe 'Copy-Item2' {
Context 'When -Path is a folder with files and subfolders' { Context 'When -Path is a folder with files and subfolders' {
BeforeAll { BeforeAll {

27
Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md

@ -145,5 +145,28 @@ deletes them.
gate. gate.
- File servers that aren't Windows, such as the IBM ESS system of #34: - File servers that aren't Windows, such as the IBM ESS system of #34:
only the feedback of the reporter covers them. only the feedback of the reporter covers them.
- The package that CI publishes for the tag: the live tests run against it
after the release, with `-Version 5.0.0-rc6`. ## After the release
The tag `5.0.0-rc6` on `b51d970`, the merge of #115, published the package
to the PowerShell Gallery on 2026-10-08 at 20:40 UTC. The **Release** job
failed after the upload: `Publish-PSResource` stopped waiting for the
Gallery after 100 seconds while the Gallery accepted the package, and its
second attempt got the error 409, "already exists". So the job didn't
create the GitHub release; rerunning the failed job creates it, as
[If a release fails](../../Docs/Contributing/05-Releasing.md#if-a-release-fails)
describes.
`Invoke-NTFSSecurityLabTest.ps1 -Version 5.0.0-rc6` downloaded the package
from the Gallery, checked it against the SHA-512 that the Gallery
publishes, and ran in both editions, 20:43 to 21:00 UTC. The SHA-256 of
`NTFSSecurity.5.0.0-rc6.nupkg` is
`83EBCADEE0698A9523661352A69B9D25F6EB2903F45A6C4FAB36F5BF4B139100`.
The live tests came from the branch of 5.0.0-rc7, which expects the
warning of `Get-NTFSEffectiveAccess` for a computer that can't be reached
to name the computer. The package failed only that test, in the role Admin
of both editions, with exactly the text that the live tests of 5.0.0-rc6
expect; every other test passed: Delegate 38, ServerAdmin 13, Admin 39,
and Server 72 with 1 skipped, in each edition. The fixture was removed at
21:03 UTC, and its removal checked as above.

149
Tests/Lab/Acceptance-2026-10-08-5.0.0-rc7.md

@ -0,0 +1,149 @@
# Lab acceptance of 5.0.0-rc7
Acceptance of the release candidate 5.0.0-rc7 in the lab, on 2026-10-08,
before the pull request. It follows the procedure in the
[README](README.md#acceptance-of-a-release-candidate).
The candidate holds the behavior changes that Phase 2 of the quality gate
found, which the agent decided as assumptions for the maintainer's review,
and the fixes of one review. A run of the code of `4ee01e5`, packaged
before the commit that sets the label, so that it reported 5.0.0-rc6,
passed; the review then led to `7936d9f` to `dc6e9f5`. The first run of
`dc6e9f5` started without the checkpoint of step 3, so it was repeated
after the checkpoint. This record describes the repeated run of
`dc6e9f5`, the last commit of the branch that changes the module, and
names the results of the earlier runs.
## Candidate
- Branch `ai/release-5.0.0-rc7`, commit
`dc6e9f5359bb02bd173dc2174dd7bca8add2df86`, 10 commits on `be04cb7`
(the head of the pull request of 5.0.0-rc6, #115).
- Release build of that commit, packaged with
`.github\scripts\New-ModulePackage.ps1`. The live tests imported the
module from the extracted `NTFSSecurity.zip`. CI builds the packages that
the tag publishes again, so their hashes differ; the live tests run once
more against the published package.
| SHA-256 | File |
| --- | --- |
| `2273126D91A1A737F0EDF8350A7E90FFC4FD7CB6A1AEA8306874FC8517349C3C` | `NTFSSecurity.5.0.0-rc7.nupkg` |
| `38B39930D92EF3FCD03FD60E05E678D5645C77BB34EA677E053F78F779ECE668` | `NTFSSecurity.zip` |
| `710C83A498700AA36DAE02272E2556EEC58655DE7CA5570CE870F64DFBAF53A9` | `NTFSSecurity\NTFSSecurity.dll` |
| `8876D0AFE2156581FE31369982DD9A117FFE38C52179409FC11AC95A9A3D68C1` | `NTFSSecurity\Security2.dll` |
| `902157ABBD2E0B76DA744A918BDD174D5226C3494908ABA75F9E5DE28AE6A008` | `NTFSSecurity\ProcessPrivileges.dll` |
| `E2077AFEB38703345AE7857C1266F8B26E167ED887BFFAC8C8169A8F267BE6E9` | `NTFSSecurity\PrivilegeControl.dll` |
| `A8DA47194AB0F71232C69D01955AD93BA73C7ECEB58D0DE800CA085D4A2E18D8` | `NTFSSecurity\AlphaFS.dll` |
| `968514234BAAF16789A560C86A6F701F69E91262F1EAB62142DDF6D05A8D2A52` | `NTFSSecurity\NTFSSecurity.psd1` |
| `3F777E9D141EE0046119DA9D5ECF88A3BC7E023726098FE2FB150528E2FB59B8` | `NTFSSecurity\NTFSSecurity.psm1` |
| `59583423241951EBE0FC2D8237D0C28C3ECC8C7CD2C115D2660F8579888632FC` | `NTFSSecurity\NTFSSecurity.Init.ps1` |
| `FB0920CC37ED858F55AFD54998DC854E27FBBE6A0177CB059CB03CFE91361197` | `NTFSSecurity\NTFSSecurity.format.ps1xml` |
| `CB6882FF91E6716605D5599E7B464C3346E461216ACED07E847621738F04FB9B` | `NTFSSecurity\NTFSSecurity.types.ps1xml` |
| `3550E659932EE61A96C6049477C14FE08F131114104BBCB9551C4B1AEA605816` | `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml` |
## Tests without a lab
The Pester suite of `1063b29`, 712 tests, against the same build; the
commits after `dc6e9f5` change only tests. No test failed, and every test
ran in at least one configuration.
| Configuration | Passed | Failed | Skipped |
| --- | ---: | ---: | ---: |
| Windows PowerShell 5.1, elevated | 688 | 0 | 24 |
| PowerShell 7, elevated | 658 | 0 | 54 |
| Windows PowerShell 5.1, basic user | 612 | 0 | 100 |
| PowerShell 7, basic user | 582 | 0 | 130 |
## Lab
`WindowsAccessControlLab` (AutomatedLab on Hyper-V). Every machine runs
Windows Server 2025 Datacenter (10.0.26100).
| Machine | Domain | Role in the tests |
| --- | --- | --- |
| `F1ADC1` | `a.forest1.net` | Domain controller of the accounts |
| `F1AFile1` | `a.forest1.net` | Client that runs the tests |
| `F1AFile2` | `a.forest1.net` | File server with the share |
| `F1BDC1` | `b.forest1.net` | Account of another domain of the forest |
| `F2DC1` | `forest2.net` | Account of another forest |
| `F3DC1` | `forest3.net` | Account of another forest |
Readiness, checked before the run: WinRM answered on all six machines.
The four domain controllers answered LDAP (RootDSE, synchronized) and
issued a Kerberos ticket for `krbtgt`. The client and the file server
found a domain controller, had a working secure channel, and got a service
ticket for each other. The clocks were 4.2 to 4.9 seconds ahead of the
host.
Checkpoints (Production) of the six machines, taken before the run:
`ntfs-rc7-dc6e9f5-before-acceptance` (16:24 UTC).
## Results
`Invoke-NTFSSecurityLabTest.ps1 -ModulePath <extracted package>` in both
editions, 16:24 to 16:40 UTC. The module reported version 5.0.0-rc7 in
every role that loads it.
| Edition | Role | Passed | Failed | Skipped |
| --- | --- | ---: | ---: | ---: |
| Windows PowerShell 5.1 | Delegate | 38 | 0 | 0 |
| Windows PowerShell 5.1 | ServerAdmin | 13 | 0 | 0 |
| Windows PowerShell 5.1 | Admin | 40 | 0 | 0 |
| Windows PowerShell 5.1 | Server | 72 | 0 | 1 |
| PowerShell 7 | Delegate | 38 | 0 | 0 |
| PowerShell 7 | ServerAdmin | 13 | 0 | 0 |
| PowerShell 7 | Admin | 40 | 0 | 0 |
| PowerShell 7 | Server | 72 | 0 | 1 |
The role Server skips the check of the module version, because it doesn't
load the module. The accounts of the other domain and forests were
`B\NtfsLiveForeign`, `forest2\NtfsLiveForeign`, and
`forest3\NtfsLiveForeign`; their 13 tests passed in both editions.
The earlier runs had the same counts and no failure: the code of
`4ee01e5` from 15:32 to 15:49 UTC, and the first run of `dc6e9f5`, without
a checkpoint, from 16:04 to 16:20 UTC.
## Baseline
The published 5.0.0-rc6 from the PowerShell Gallery, whose hash the script
checks against the one the Gallery publishes, in both editions, 20:43 to
21:00 UTC, with the same live tests. It failed one test in each edition,
the warning of `Get-NTFSEffectiveAccess` for a computer that can't be
reached, which names the computer since 5.0.0-rc7: Admin 39 passed and 1
failed; Delegate 38, ServerAdmin 13, and Server 72 with 1 skipped passed.
The live tests find no other difference between the two versions; the
local tests cover the other changes of 5.0.0-rc7.
## Cleanup
`Invoke-NTFSSecurityLabTest.ps1 -RemoveFixture` at 16:21 UTC, after the
first two runs, at 16:44 UTC, after the acceptance, and at 21:03 UTC,
after the baseline. Each check
compared the lab with the 10 SIDs of the fixture's accounts and groups,
read before the removal, which found the fixture. After each removal:
- No domain has the organizational unit `NTFSSecurityLive` or an account
whose name starts with `NtfsLive`.
- The file server has no share `NTFSSecurityLive`, no folder
`C:\NTFSSecurityLive` or `C:\NTFSSecurityLab`, and no local group
`NtfsLiveLocal`.
- The client has no folder `C:\NTFSSecurityLab`.
- On both machines, Administrators, Access Control Assistance Operators,
and Remote Management Users have no member of the fixture, and no
profile of the fixture's accounts is left.
The checkpoint stays on the six machines until the maintainer deletes it.
## Not covered
- Other operating systems than Windows Server 2025, such as a Windows 11
client and Server 2019 or 2022 file servers: Phase 3 of the quality
gate.
- File servers that aren't Windows, such as the IBM ESS system of #34:
only the feedback of the reporter covers them.
- A folder that `Move-Item2` moves from the client to the share, another
volume on another computer: the local tests move folders to
`\\localhost\C$`, another volume over SMB on the same computer.
- The package that CI publishes for the tag: the live tests run against it
after the release, with `-Version 5.0.0-rc7`.

7
Tests/Lab/NTFSSecurity.Live.Tests.ps1

@ -408,13 +408,14 @@ Describe 'Get-NTFSEffectiveAccess for a domain account on a share folder' -Tag '
} }
# The cmdlet page: when the remote authorization manager can't be reached, the cmdlet falls back to the local one # The cmdlet page: when the remote authorization manager can't be reached, the cmdlet falls back to the local one
# and warns that the result may be inaccurate. # and warns that the result may be inaccurate; since 5.0.0-rc7, the warning names the computer.
It 'Should fall back to the authorization manager of the client and warn when -ServerName can''t be reached' { It 'Should fall back to the authorization manager of the client and warn when -ServerName can''t be reached' {
$result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $configuration.UnreachableServerName -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue) $result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $configuration.UnreachableServerName -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$operationWarnings.Message | Should -Contain ('The effective rights can only be computed based on group membership on this computer. ' + $operationWarnings.Message | Should -Contain ('The effective rights can only be computed based on group membership on this computer, ' +
'For more accurate results, calculate effective access rights on the target computer') "because the computer '$($configuration.UnreachableServerName)' can't be reached for a remote access check. " +
'For more accurate results, calculate effective access rights on that computer.')
$result | Should -HaveCount 1 $result | Should -HaveCount 1
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights) Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights)
} }

3
Tests/Lab/README.md

@ -141,7 +141,8 @@ and record the evidence in this folder:
5. Remove the fixture with `-RemoveFixture` and check that its accounts, 5. Remove the fixture with `-RemoveFixture` and check that its accounts,
share, folders, group memberships, and profiles are gone. share, folders, group memberships, and profiles are gone.
Records: [5.0.0-rc6](Acceptance-2026-10-08-5.0.0-rc6.md). Records: [5.0.0-rc6](Acceptance-2026-10-08-5.0.0-rc6.md),
[5.0.0-rc7](Acceptance-2026-10-08-5.0.0-rc7.md).
## Files ## Files

168
Tests/Links.Tests.ps1

@ -10,9 +10,7 @@ BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege' $canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
# The administrative share of the drive of the sandboxes reaches them over SMB, like a share of a file server. # The administrative share of the drive of the sandboxes reaches them over SMB, like a share of a file server.
$tempPath = [IO.Path]::GetTempPath() $canUseAdminShare = Test-AdminShareAvailable
$canUseAdminShare = (Test-IsElevated) -and
(Test-Path -LiteralPath ('\\localhost\{0}$\' -f $tempPath.Substring(0, 1)) -ErrorAction SilentlyContinue)
} }
BeforeAll { BeforeAll {
@ -21,13 +19,6 @@ BeforeAll {
Import-Module -Name $modulePath -Force -ErrorAction Stop Import-Module -Name $modulePath -Force -ErrorAction Stop
$sandbox = New-TestSandbox -Name 'Links' $sandbox = New-TestSandbox -Name 'Links'
Push-Location -LiteralPath $sandbox Push-Location -LiteralPath $sandbox
function ConvertTo-AdminSharePath {
# The path of a sandbox item on the administrative share of its drive, such as \\localhost\C$\...
param ([string] $Path)
'\\localhost\{0}${1}' -f $Path.Substring(0, 1), $Path.Substring(2)
}
} }
AfterAll { AfterAll {
@ -122,6 +113,38 @@ Describe 'New-NTFSHardLink' {
$link | Should -Not -Exist $link | Should -Not -Exist
} }
# Before 5.0.0-rc7, an existing -Path, a missing -Target, or a folder as -Target stopped the pipeline with a
# terminating error, so the links that followed weren't created.
It 'Should write a non-terminating error for each link that it cannot create and continue with the next one' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueExisting'
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFolder' -Directory
$missing = Join-Path -Path $sandbox -ChildPath 'ContinueMissing.txt'
$missingLink = Join-Path -Path $sandbox -ChildPath 'ContinueMissingLink.txt'
$folderLink = Join-Path -Path $sandbox -ChildPath 'ContinueFolderLink.txt'
$link = Join-Path -Path $sandbox -ChildPath 'ContinueLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $missingLink, $folderLink, $link
$requests = @(
[pscustomobject]@{ Path = $existing; Target = $target }
[pscustomobject]@{ Path = $missingLink; Target = $missing }
[pscustomobject]@{ Path = $folderLink; Target = $folder }
[pscustomobject]@{ Path = $link; Target = $target }
)
$requests | New-NTFSHardLink -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 3
$linkErrors | ForEach-Object -Process { $_.FullyQualifiedErrorId | Should -BeLike 'CreateHardLinkError,*' }
($linkErrors | ForEach-Object -Process { $_.CategoryInfo.Category }) -join ',' | Should -Be 'ResourceExists,ObjectNotFound,InvalidArgument'
($linkErrors | ForEach-Object -Process { $_.TargetObject }) -join '|' | Should -Be (($existing, $missingLink, $folderLink) -join '|')
# Since 5.0.0-rc7, the error for a folder as -Target names the folder.
$linkErrors[2].Exception.Message | Should -BeLike ("*'{0}'*" -f [WildcardPattern]::Escape($folder))
$link | Should -Exist
$missingLink | Should -Not -Exist
$folderLink | Should -Not -Exist
Get-Content -LiteralPath $existing | Should -Be 'ContinueExisting'
}
# Windows can't list the names of a file on a network share. Before 5.0.0-rc6, the cmdlet stopped with a # Windows can't list the names of a file on a network share. Before 5.0.0-rc6, the cmdlet stopped with a
# terminating error after it had created the link. # terminating error after it had created the link.
It 'Should create the link on a network share and write an error for -PassThru, which cannot list the names there' -Skip:(-not $canUseAdminShare) { It 'Should create the link on a network share and write an error for -PassThru, which cannot list the names there' -Skip:(-not $canUseAdminShare) {
@ -129,7 +152,7 @@ Describe 'New-NTFSHardLink' {
$link = Join-Path -Path $sandbox -ChildPath 'ShareLink.txt' $link = Join-Path -Path $sandbox -ChildPath 'ShareLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = @(New-NTFSHardLink -Path (ConvertTo-AdminSharePath -Path $link) -Target (ConvertTo-AdminSharePath -Path $target) -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue) $result = @(New-NTFSHardLink -Path (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $link) -Target (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $target) -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue)
$link | Should -Exist $link | Should -Exist
$linkErrors | Should -HaveCount 1 $linkErrors | Should -HaveCount 1
@ -205,7 +228,7 @@ Describe 'Get-NTFSHardLink' {
It 'Should write an error for a file on a network share and continue with the next path' -Skip:(-not $canUseAdminShare) { It 'Should write an error for a file on a network share and continue with the next path' -Skip:(-not $canUseAdminShare) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareFile' $file = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareFile'
$other = New-TestSandboxItem -Sandbox $sandbox -Name 'AfterShare' $other = New-TestSandboxItem -Sandbox $sandbox -Name 'AfterShare'
$sharePath = ConvertTo-AdminSharePath -Path $file $sharePath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file
$result = @(Get-NTFSHardLink -Path $sharePath, $other -ErrorVariable linkErrors -ErrorAction SilentlyContinue) $result = @(Get-NTFSHardLink -Path $sharePath, $other -ErrorVariable linkErrors -ErrorAction SilentlyContinue)
@ -309,17 +332,128 @@ Describe 'New-NTFSSymbolicLink' {
Test-Path2 -Path $link | Should -BeFalse Test-Path2 -Path $link | Should -BeFalse
} }
# Before 5.0.0-rc7, an existing -Path stopped the pipeline with a terminating error. The cmdlet checks the paths
# before it creates a link, so this runs without the right to create symbolic links as well.
It 'Should write a non-terminating error for an existing -Path and continue with the next link' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicContinueTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicContinueExisting'
$missing = Join-Path -Path $sandbox -ChildPath 'SymbolicContinueMissing.txt'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicContinueLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $link
$requests = @(
[pscustomobject]@{ Path = $existing; Target = $target }
[pscustomobject]@{ Path = $link; Target = $missing }
)
$requests | New-NTFSSymbolicLink -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 2
$linkErrors | ForEach-Object -Process { $_.FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*' }
($linkErrors | ForEach-Object -Process { $_.CategoryInfo.Category }) -join ',' | Should -Be 'ResourceExists,ObjectNotFound'
(Get-Item -LiteralPath $existing -Force).LinkType | Should -BeNullOrEmpty
Test-Path2 -Path $link | Should -BeFalse
}
# Windows rejects the link with error 1314 without the "Create symbolic links" user right. Its message is # Windows rejects the link with error 1314 without the "Create symbolic links" user right. Its message is
# localized, so the test compares the HRESULT of that error. # localized, so the test compares the HRESULT of that error. Before 5.0.0-rc7, the error was terminating.
It 'Should fail and create no link without the right to create symbolic links' -Skip:$canCreateSymbolicLinks { It 'Should write a non-terminating error and create no link without the right to create symbolic links' -Skip:$canCreateSymbolicLinks {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicNoRight' $target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicNoRight'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicNoRight.txt' $link = Join-Path -Path $sandbox -ChildPath 'SymbolicNoRight.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$thrown = { New-NTFSSymbolicLink -Path $link -Target $target -ErrorAction Stop } | Should -Throw -PassThru New-NTFSSymbolicLink -Path $link -Target $target -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$thrown.FullyQualifiedErrorId | Should -BeLike '*,NTFSSecurity.NewSymbolicLink' $linkErrors | Should -HaveCount 1
'0x{0:X8}' -f $thrown.Exception.HResult | Should -Be '0x80070522' $linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*'
'0x{0:X8}' -f $linkErrors[0].Exception.HResult | Should -Be '0x80070522'
Test-Path2 -Path $link | Should -BeFalse Test-Path2 -Path $link | Should -BeFalse
} }
} }
# Each error names its item, so that the errors of many links can be told apart. Before 5.0.0-rc7, the errors of
# New-NTFSSymbolicLink for an existing -Path and a missing -Target named no path. No link is created, so the tests run
# without the right to create symbolic links as well.
Describe 'Errors of the cmdlets that create links' {
It '<Command> should name the existing -Path and the missing -Target in its errors' -ForEach @(
@{ Command = 'New-NTFSHardLink' }
@{ Command = 'New-NTFSSymbolicLink' }
) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'NamedExisting'
$missing = Join-Path -Path $sandbox -ChildPath ('NamedMissing-{0}.txt' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
$link = Join-Path -Path $sandbox -ChildPath ('NamedLink-{0}.txt' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $link
$requests = @(
[pscustomobject]@{ Path = $existing; Target = $target }
[pscustomobject]@{ Path = $link; Target = $missing }
)
$requests | & $Command -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 2
$linkErrors[0].Exception.Message | Should -BeLike ("*'{0}'*" -f [WildcardPattern]::Escape($existing))
$linkErrors[1].Exception.Message | Should -BeLike ("*'{0}'*" -f [WildcardPattern]::Escape($missing))
Test-Path2 -Path $link | Should -BeFalse
}
# Both cmdlets check -Path before -Target. Before 5.0.0-rc7, New-NTFSSymbolicLink checked -Target first and
# reported a missing target instead.
It '<Command> should report an existing -Path before a missing -Target' -ForEach @(
@{ Command = 'New-NTFSHardLink' }
@{ Command = 'New-NTFSSymbolicLink' }
) {
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'FirstExisting'
$missing = Join-Path -Path $sandbox -ChildPath ('FirstMissing-{0}.txt' -f [guid]::NewGuid().ToString('N').Substring(0, 8))
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing
& $Command -Path $existing -Target $missing -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 1
$linkErrors[0].CategoryInfo.Category | Should -Be 'ResourceExists'
$linkErrors[0].TargetObject | Should -Be $existing
Get-Content -LiteralPath $existing | Should -Be 'FirstExisting'
}
}
# Windows PowerShell rejects a character that Windows doesn't allow in a path, such as |, before Windows sees the path.
# Before 5.0.0-rc7, that stopped the pipeline in Windows PowerShell, and PowerShell 7 reported it as a WriteError.
Describe 'Paths that Windows does not allow in the cmdlets that create links' {
It '<Command> should write a non-terminating InvalidArgument error and continue with the next link' -ForEach @(
@{ Command = 'New-NTFSHardLink'; ErrorId = 'CreateHardLinkError' }
@{ Command = 'New-NTFSSymbolicLink'; ErrorId = 'CreateSymbolicLinkError' }
) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'InvalidExisting'
$invalid = Join-Path -Path $sandbox -ChildPath 'Invalid|Link.txt'
# The second link exists, so that the cmdlet rejects it before it creates anything, also without the right to
# create symbolic links; its error shows that the cmdlet went on.
$requests = @(
[pscustomobject]@{ Path = $invalid; Target = $target }
[pscustomobject]@{ Path = $existing; Target = $target }
)
$requests | & $Command -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 2
$linkErrors | ForEach-Object -Process { $_.FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" }
($linkErrors | ForEach-Object -Process { $_.CategoryInfo.Category }) -join ',' | Should -Be 'InvalidArgument,ResourceExists'
$linkErrors[0].TargetObject | Should -Be $invalid
Get-Content -LiteralPath $existing | Should -Be 'InvalidExisting'
}
}
# Before 5.0.0-rc7, both parameters were optional. Without -Path, the cmdlets failed with an index error; without -Target,
# they used the current location, so New-NTFSSymbolicLink -Path Link created a link to the current folder.
Describe 'Parameters of the cmdlets that create links' {
It '<Command> should require -<Parameter>' -ForEach @(
@{ Command = 'New-NTFSHardLink'; Parameter = 'Path' }
@{ Command = 'New-NTFSHardLink'; Parameter = 'Target' }
@{ Command = 'New-NTFSSymbolicLink'; Parameter = 'Path' }
@{ Command = 'New-NTFSSymbolicLink'; Parameter = 'Target' }
) {
$sets = @((Get-Command -Name $Command).Parameters[$Parameter].ParameterSets.Values)
$sets | Should -Not -BeNullOrEmpty
$sets | ForEach-Object -Process { $_.IsMandatory | Should -BeTrue }
}
}

2
Tests/Repository.Tests.ps1

@ -97,7 +97,7 @@ Describe 'Release metadata' {
# The PowerShell Gallery doesn't accept a version twice. Add every published version to this list # The PowerShell Gallery doesn't accept a version twice. Add every published version to this list
# (Docs/Contributing/05-Releasing.md). # (Docs/Contributing/05-Releasing.md).
It 'Should not reuse a version that the PowerShell Gallery already has' { It 'Should not reuse a version that the PowerShell Gallery already has' {
$publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5' $publishedVersions = '4.0', '4.2.2', '4.2.3', '4.2.4', '4.2.5', '4.2.6', '5.0.0-rc1', '5.0.0-rc2', '5.0.0-rc3', '5.0.0-rc4', '5.0.0-rc5', '5.0.0-rc6'
$publishedVersions | Should -Not -Contain $version $publishedVersions | Should -Not -Contain $version
} }

36
Tests/TestHelpers.Tests.ps1

@ -13,6 +13,8 @@ BeforeDiscovery {
$canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege' $canAssignAnyOwner = Test-PrivilegeHeld -Name 'SeRestorePrivilege'
# Reading and writing audit entries needs the Security privilege. # Reading and writing audit entries needs the Security privilege.
$holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' $holdsSecurityPrivilege = Test-PrivilegeHeld -Name 'SeSecurityPrivilege'
$isElevated = Test-IsElevated
$canUseAdminShare = Test-AdminShareAvailable
} }
BeforeAll { BeforeAll {
@ -268,4 +270,38 @@ Describe 'Test helpers' {
Test-PrivilegeHeld -Name 'SeNoSuchPrivilege' | Should -BeFalse Test-PrivilegeHeld -Name 'SeNoSuchPrivilege' | Should -BeFalse
} }
} }
# The administrative share of a drive, such as \\localhost\C$, is a network path and another volume for Windows.
Context 'ConvertTo-TestAdminSharePath and Test-AdminShareAvailable' {
BeforeAll {
$sandbox = New-TestSandbox -Name 'Helpers'
}
AfterAll {
Remove-TestSandbox -Sandbox $sandbox
}
It 'Should return the path of an item in the sandbox on the administrative share of its drive' {
$path = Join-Path -Path $sandbox -ChildPath 'Folder\File.txt'
ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $path |
Should -Be ('\\localhost\' + $path.Substring(0, 1) + '$' + $path.Substring(2))
}
It 'Should reach the same item through the administrative share' -Skip:(-not $canUseAdminShare) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AdminShare'
Get-Content -LiteralPath (ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file) | Should -Be 'AdminShare'
}
It 'Should refuse a path outside the sandbox' {
{ ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path "$sandbox-Other\File.txt" } |
Should -Throw -ExpectedMessage 'Refusing to change*'
}
# Only administrators can open the administrative shares.
It 'Should not offer the administrative share without elevation' -Skip:$isElevated {
Test-AdminShareAvailable | Should -BeFalse
}
}
} }

43
Tests/TestHelpers.psm1

@ -381,6 +381,47 @@ function Test-PrivilegeHeld {
Where-Object -Property Name -EQ -Value $Name) Where-Object -Property Name -EQ -Value $Name)
} }
function Test-AdminShareAvailable {
<#
.SYNOPSIS
Returns $true when the process can open the administrative share of the drive of the sandboxes, such as
\\localhost\C$, which Windows treats as a network path and as another volume. Only administrators can.
#>
[CmdletBinding()]
[OutputType([bool])]
param ()
$drive = [IO.Path]::GetTempPath().Substring(0, 1)
(Test-IsElevated) -and [bool] (Test-Path -LiteralPath ('\\localhost\{0}$\' -f $drive) -ErrorAction SilentlyContinue)
}
function ConvertTo-TestAdminSharePath {
<#
.SYNOPSIS
Returns the path of an item in a sandbox on the administrative share of its drive, such as
\\localhost\C$\Users\...\File.txt. It checks the local path with Assert-TestSandboxPath first, so that a test
reaches only items of its sandbox through the share.
.PARAMETER Sandbox
The sandbox folder that New-TestSandbox returned.
.PARAMETER Path
The full local path of the item.
#>
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(Mandatory)]
[string]
$Sandbox,
[Parameter(Mandatory)]
[string]
$Path
)
Assert-TestSandboxPath -Sandbox $Sandbox -Path $Path
'\\localhost\{0}${1}' -f $Path.Substring(0, 1), $Path.Substring(2)
}
Export-ModuleMember -Function New-TestSandbox, Assert-TestSandboxPath, Remove-TestSandbox, New-TestSandboxItem, Export-ModuleMember -Function New-TestSandbox, Assert-TestSandboxPath, Remove-TestSandbox, New-TestSandboxItem,
Block-TestReadPermission, Block-TestWritePermission, Add-TestDenyRule, Set-TestOwner, Test-IsElevated, Block-TestReadPermission, Block-TestWritePermission, Add-TestDenyRule, Set-TestOwner, Test-IsElevated,
Test-PrivilegeHeld Test-PrivilegeHeld, Test-AdminShareAvailable, ConvertTo-TestAdminSharePath

Loading…
Cancel
Save