Browse Source

Merge pull request #106 from raandree/ai/release-5.0.0-rc2

chore(release): prepare 5.0.0-rc2
pull/112/head 5.0.0-rc2
Raimund Andrée 6 days ago
committed by GitHub
parent
commit
7ddda8d4c5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 85
      .memory-bank/activeContext.md
  2. 234
      .memory-bank/progress.md
  3. 69
      .memory-bank/systemPatterns.md
  4. 5
      .memory-bank/techContext.md
  5. 7
      CHANGELOG.md
  6. 4
      Docs/Contributing/05-Releasing.md
  7. 4
      Docs/README.md
  8. 4
      NTFSSecurity/NTFSSecurity.psd1
  9. 34
      Security2/FileSystem/FileSystemInheritanceInfo.cs
  10. 53
      Tests/Inheritance.Tests.ps1
  11. 32
      Tests/Repository.Tests.ps1

85
.memory-bank/activeContext.md

@ -1,6 +1,6 @@
---
status: current
last-verified: 2026-10-04
last-verified: 2026-10-05
owner: active-agent
source: current task evidence
---
@ -9,62 +9,47 @@ source: current task evidence
## Current focus
Overnight run 2026-10-04/05 (autopilot, maintainer asleep): fix the code
defects A to D of `progress.md`, implement the maintainer's E decisions,
triage the 37 open issues, and prepare 5.0.0-rc2. Eight stacked local
branches, each a PR against `master`, to be merged in this order with merge
commits: `ai/maintenance`, `ai/defects-a`, `ai/defects-b`, `ai/defects-c`,
`ai/defects-d`, `ai/decisions-e`, `ai/issue-fixes`,
`ai/release-5.0.0-rc2`. Nothing is pushed; the maintainer pushes, opens
the PRs, and tags `5.0.0-rc2` after the merges.
The overnight run of 2026-10-04/05 is finished. 5.0.0-rc2 waits on eight
stacked local branches (see `progress.md`): the maintainer pushes them,
opens one PR each against `master`, merges them in order with merge
commits, and tags `5.0.0-rc2` on the last merge commit once CI on `master`
is green. The run's report lists the commands, the PR descriptions, a
reply for each issue, and the open questions.
## Maintainer decisions for the run (2026-10-04)
- D1: the fixes ship in 5.0.0; entries go under `[Unreleased]` (`Fixed`;
intended behavior changes under `Changed` with the way back). The last
PR sets `Prerelease = 'rc2'`.
- D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL;
the page states the result and the risk.
- D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets.
- D1: the fixes ship in 5.0.0, under `[Unreleased]`; the behavior changes
of D3 and D4 are listed under `Changed` with the way back.
- D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL.
- D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets
(Decision 13).
- D4: `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`, with
the `*AccessRules` names as aliases.
- D5: `Get-FileHash2` works in PowerShell 7 for every algorithm .NET has;
a missing one fails only when requested.
- D7: Dependabot for `github-actions` only; AlphaFS 2.2.1 in
`NTFSSecurity\packages.config` (no upgrade, no changelog entry).
the old names as aliases.
- D5: `Get-FileHash2` works in PowerShell 7; `MACTripleDES` is deprecated.
- D6: only reproducible bugs are fixed; other behavior changes are
questions for the maintainer.
- D7: Dependabot for `github-actions` only; AlphaFS 2.2.1 everywhere.
- D8: the manifest `Description` and a version-neutral README.
- D9: merged local branches deleted after the run.
- 2026-10-05: the report's recommendations accepted. #5 and #82 ship in
5.0.0 as breaking changes; #34 for rc3 if a file server is available;
#41 and #90 after 5.0.0; Minor findings become issues; the `pwsh` crash
is watched in CI.
## Evidence
- Baseline at `e0f5366` (Release build, workstation): Windows PowerShell
261 passed, 7 skipped; PowerShell 7 232 passed, 36 skipped (268 tests).
- `ai/maintenance` adds `Tests\Repository.Tests.ps1` (8 tests): Windows
PowerShell 269 passed, 7 skipped; PowerShell 7 240 passed, 36 skipped.
Review: Dependabot PRs ran unreviewed actions in a job with
`contents: write`; the wiki preview is now read-only (`publish-wiki`).
- `ai/defects-a` fixes defects 1 to 13 and the same repeat bug in
`Get-NTFSAccess` (found with 4); its review fixes are in the last
commit: Windows PowerShell 312 passed, 17 skipped; PowerShell 7 282
passed, 47 skipped (329 tests).
- `ai/defects-b` fixes defects 14 to 17 (`-AppliesTo` is mandatory in the
`Simple` sets; `-RemoveSpecific` is back): Windows PowerShell 333 passed,
19 skipped; PowerShell 7 303 passed, 49 skipped (352 tests).
- `ai/defects-c` fixes defects 18 to 21, the same missing `continue` in
`Remove-NTFSAudit`, and a stale hash in `Get-FileHash2`. Its review
found that a failed retry after taking ownership left the owner changed;
`BaseCmdlet.InvokeAsOwner` now restores it: Windows PowerShell 356
passed, 20 skipped; PowerShell 7 325 passed, 51 skipped (376 tests).
- `ai/defects-d` fixes defects 22 to 24 and `-PassThru` under `-WhatIf` in
the `*-Item2` cmdlets: Windows PowerShell 379 passed, 23 skipped;
PowerShell 7 348 passed, 54 skipped (402 tests).
- `ai/decisions-e` implements D2 to D5 (Decision 13): Windows PowerShell
395 passed, 26 skipped; PowerShell 7 366 passed, 55 skipped (421
tests). `Get-FileHash2` tests now run in PowerShell 7 as well.
- `ai/issue-fixes` fixes #3, #86, and #88 and adds `Docs/FAQ.md`: Windows
PowerShell 402 passed, 26 skipped; PowerShell 7 373 passed, 55 skipped
(428 tests).
- Every branch tip: Release build without new warnings (296 at the top,
305 at the baseline), docs checks clean, package dry run passed.
- Tests at the top branch: Windows PowerShell 423 passed, 26 skipped;
PowerShell 7 394 passed, 55 skipped (449). The baseline had 268 tests.
- Tests that need privileges skip on the workstation and run in CI only;
the PR descriptions list them.
- Reviews: one security review per PR; the Major findings were fixed in
the PR that had them (PR 1: 1, PR 2: 5, PR 4: 2, PR 6: 1, PR 7: 3,
PR 8: 2).
## Next step
5.0.0-rc2 on `ai/release-5.0.0-rc2`.
The maintainer applies the repository settings, pushes the eight branches,
opens and merges the PRs in order, and tags `5.0.0-rc2` once CI on
`master` is green.

234
.memory-bank/progress.md

@ -1,6 +1,6 @@
---
status: current
last-verified: 2026-10-04
last-verified: 2026-10-05
owner: active-agent
source: repository evidence
---
@ -9,188 +9,76 @@ source: repository evidence
## Current status
PRs #91 to #98 are merged. CI published the prerelease 5.0.0-rc1 from
`master` (`e0f5366`, tag `5.0.0-rc1`) to the PowerShell Gallery and GitHub;
the stable Gallery version is still 4.2.6. CI runs on GitHub Actions:
build, docs checks, tests in Windows PowerShell 5.1 and PowerShell 7,
packages, the wiki generated from `Docs` (43 pages), and releases on a
version tag (Decision 12). Next: the maintainer tests 5.0.0-rc1.
5.0.0-rc2 is ready on eight stacked local branches from the overnight run of
2026-10-04/05; none is pushed. Each is a PR against `master`, merged in
this order with merge commits: `ai/maintenance`, `ai/defects-a`,
`ai/defects-b`, `ai/defects-c`, `ai/defects-d`, `ai/decisions-e`,
`ai/issue-fixes`, `ai/release-5.0.0-rc2`. The tag `5.0.0-rc2` on the last
merge commit then publishes it through CI (Decision 12). `master`
(`e0f5366`) carries 5.0.0-rc1, published on 2026-10-04; the stable Gallery
version is still 4.2.6.
## Recent milestones
- 2026-10-02 to 2026-10-04: #91 aligned the docs with the code (Decision
6; #83 closed as superseded), #92 did housekeeping (Decision 7), and
#93 shipped the generated help file (Decision 8, `Tests\Help.Tests.ps1`).
- 2026-10-04: #94 to #96 dropped Read the Docs (Decision 9), set version
5.0.0 with a valid manifest (Decision 10), and moved CI and a wiki
generated from `Docs` to GitHub Actions (Decision 11). #97 completed the
version history, kept separate from `CHANGELOG.md`, from the six Gallery
packages and the commit history.
- 2026-10-02 to 2026-10-04: #91 to #97 aligned the docs with the code,
shipped the help file, kept the docs on GitHub, set version 5.0.0, moved
CI and a wiki generated from `Docs` to GitHub Actions, and completed the
version history (Decisions 6 to 11).
- 2026-10-04: #98 (`e0f5366`) added releases on a version tag through CI
(Decision 12), with a prerelease first; `New-ModulePackage.ps1` adds the
command tags that PSResourceGet drops. The tag `5.0.0-rc1` (run
37230802387) published to the Gallery at 20:12 UTC and created the
GitHub prerelease. Verified: the Gallery nupkg and the GitHub zip are
byte-identical to the CI artifacts, the DLLs are optimized Release
builds, the Gallery shows the prerelease flag, the release notes link,
and all 36 `PSCmdlet_` and `PSCommand_` tags, and the installed module
passes the full suite (Windows PowerShell 261 passed, 7 skipped;
PowerShell 7 232 passed, 36 skipped).
(Decision 12). The tag `5.0.0-rc1` published to the Gallery and created
the GitHub prerelease; the installed module passed the full suite.
- 2026-10-05, overnight run (local branches): the 24 code defects of work
package 5 fixed with regression tests, plus what the reviews found: a
failed retry after taking ownership left the owner changed, and
`-PassThru` wrote objects after a failed change or under `-WhatIf`.
Maintainer decisions D2 to D5 (Decision 13), the issues #3, #4, #17, #74,
#86, and #88 fixed, the 37 open issues triaged, `Docs/FAQ.md`, Dependabot for
the actions, and the prerelease label `rc2`. One security review per PR;
every Major finding fixed.
- 2026-10-05, morning: the maintainer accepted the report's
recommendations. #5 (`Get-ChildItem2 -Attributes` matches any listed
attribute; an empty value is an error) and #82 (no `Size` alias) ship in
5.0.0 as breaking changes on `ai/issue-fixes`. Tests at the top branch:
Windows PowerShell 423 passed, 26 skipped; PowerShell 7 394 passed, 55
skipped (449).
## Stable capabilities
- 36 cmdlets: access (7), audit (5), inheritance (6), owner and security
descriptor (4), privileges (3), long-path items (6), links, hash, and
disk space (5).
- Works in Windows PowerShell 5.1 and PowerShell 7 (smoke-tested), except
`Get-FileHash2`, which fails in PowerShell 7 (missing `RIPEMD160` type).
- Works in Windows PowerShell 5.1 and PowerShell 7. In PowerShell 7,
`Get-FileHash2` lacks `RIPEMD160` and `MACTripleDES`, which .NET lacks
(on `master` it still fails there for every algorithm).
- Pester tests in `Tests\` run in `$env:TEMP` sandboxes through
`Tests\TestHelpers.psm1`; tests that need privileges skip without them
and run in CI, whose runners are elevated.
## Open work
Work packages in the order agreed with the maintainer. Each gets one
`ai/<slug>` branch and PR, committed locally. The maintainer pushes and
opens the PR (the agent can't; see `techContext.md`, Constraints), and the
next package starts only after the maintainer's go-ahead.
Items 1 to 4c are done: housekeeping (#92), shipped help (#93), docs on
GitHub (#94), manifest and version 5.0.0 (#95), CI and the wiki on GitHub
Actions (#96), version history from the Gallery (#97). Optional for the
maintainer: delete the AppVeyor project and revoke its GitHub
authorization, restrict wiki editing to collaborators, and ask
`Sup3rlativ3` to delete the Read the Docs project.
4d. Release 5.0.0 through CI (Decision 12): 5.0.0-rc1 published and
verified (#98); the maintainer tests it. The final release PR comes on
release day (CI warns when the changelog date isn't that day): remove
the label, date `[Unreleased]` as `[5.0.0]`, tag `5.0.0` (steps in
`Docs/Contributing/05-Releasing.md`). Also consider the manifest
`Description` ("Windows PowerShell Module") and the `5.0.0-rc1` example
in `Docs/README.md`. Releases no longer come from a local Debug build.
4e. Repository settings, proposed to the maintainer on 2026-10-04 (not yet
agreed): the `powershell-gallery` environment has no protection rules
and no deployment policy, so a workflow on any branch can use
`PSGALLERY_API_KEY` (`nyanhp` also has write access); `master` has no
protection or ruleset; head branches aren't deleted on merge; the
remote branches `fix/#34` and `test/transfer` (2023-11-28, two commits
each) aren't merged. Dependabot for the SHA-pinned actions comes with
`ai/maintenance` (maintainer decision D7, 2026-10-04).
5. Code defects, listed below: `review: on`, one PR per group, regression
test first. Pester 5 tests import `NTFSSecurity\bin\Release`, run in a
`$env:TEMP` sandbox and in the CI workflow (pattern:
`Tests\Help.Tests.ps1`), and skip elevated cases when not elevated.
GitHub-hosted Windows runners run as administrators with UAC disabled
(GitHub docs, checked 2026-10-04), so elevated cases run in CI; the
workstation session isn't elevated. Each fix updates its cmdlet page
and `CHANGELOG.md`. Start by triaging the 37 open issues (none newer
than May 2025): #15, #47, and #66 (documentation) and #19 (fixed in
4.2.4) can be closed; #4 is defect (5); #34 has the WIP branch
`fix/#34` (`Extensions.cs`, `FileSystemSecurity2.cs`, `TestClient`);
`test/transfer` only adds a 3 MB `New.zip`. The E decisions set the
next version: fixes only 5.0.1, additions 5.1.0, changed defaults
6.0.0, unless they ship in 5.0.0 (rc2).
### Code defects (work package 5)
Numbered as agreed with the maintainer; each is documented on its page.
#### A: Crashes and wrong results (fixed on `ai/defects-a`, not merged)
- (1) `Set-NTFSInheritance` reads an unset `Nullable<bool>` when
`-AccessInheritanceEnabled` is omitted; omitted should mean unchanged.
- (2) `Get-ChildItem2` casts a file `-Path` to `DirectoryInfo`
(`InvalidCastException`).
- (3) `Get-FileHash2` returns at a folder in `-Path` and skips the rest.
- (4) `Get-NTFSAudit` re-emits the previous item's entries after a failed
path, and returns nothing without the Security privilege instead of an
error.
- (5) `Add-NTFSAudit`: `-Account` and `-AccessRights` share position 2;
use 2 and 3 like `Remove-NTFSAudit`.
- (6) `-PassThru` returns access entries in `Add-NTFSAudit` SD sets and
`Remove-NTFSAudit` Path sets.
- (7) `FileSystemAuditRule2.GetFileSystemAuditRules` takes
`InheritanceEnabled` from `AreAccessRulesProtected`.
- (8) `Get-NTFSInheritance -SecurityDescriptor` reports
`AuditInheritanceEnabled = $true` without a SACL (Path set: `$null`).
- (9) `Get-NTFSOwner`: the access-denied retry repeats the failing call,
and the catch-all turns `PipelineStoppedException` into
`ReadSecurityError`.
- (10) `Copy-Item2` fails on a folder with files
(`DirectoryNotFoundException`).
- (11) `Disable-Privileges` hits a null `privileges` field when
`EnablePrivileges = $false`.
- (12) The six inheritance cmdlets enable privileges unconditionally in
`BeginProcessing` and leave them enabled.
- (13) Format view `Children2`: the `Inherits` column uses
`IsInheritanceBlocked`, so it always shows `True` for `Get-ChildItem2`.
- Review of group A: five Major findings fixed in the last commit; the
Minor ones are listed in the PR description.
#### B: Ignored parameters and parameter sets (fixed on `ai/defects-b`, not merged)
- (14) SD sets of `Add-/Remove-NTFSAccess` and `Add-/Remove-NTFSAudit`
cannot resolve without `-AppliesTo` or the flag parameters.
- (15) `Get-NTFSEffectiveAccess`: `-ExcludeNoneAccessEntries` has no
effect, there is no output without `-Path`, and the SD set returns
nothing.
- (16) `Get-NTFSOrphanedAccess`, `Get-NTFSOrphanedAudit`, and
`Get-NTFSSimpleAccess` ignore `-Account` and `-SecurityDescriptor`;
`Get-NTFSOrphanedAudit` writes collections; `SimpleFileSystemAccessRule`
has no format view.
- (17) `removeSpecific` in `Remove-NTFSAccess/Audit` is never bound;
`Remove-NTFSAudit` leaves `appliesTo` uninitialized.
- Review of group B: no Blocker or Major; three Minor findings fixed, the
rest are listed in the PR description.
#### C: Error handling (fixed on `ai/defects-c`, not merged)
- (18) `Copy-Item2`, `Move-Item2`, `Remove-Item2`: one failing path skips
the rest of `-Path` (`return` instead of `continue`).
- (19) `Remove-NTFSAccess` continues after a failed read and writes a
second, misleading `RemoveAceError`.
- (20) The inheritance cmdlets write `-PassThru` output in `finally`, even
after a failure.
- (21) `New-NTFSHardLink` says the target path exists when it doesn't.
- Review of group C: the add and remove cmdlets also wrote `-PassThru`
after a failed change, and a failed retry after taking ownership left the
owner changed; `BaseCmdlet.InvokeAsOwner` now restores it on every path.
#### D: Metadata and cosmetics (fixed on `ai/defects-d`, not merged)
- (22) Wrong or missing `[OutputType]` (`Test-Path2`, `Get-FileHash2`,
`Add-NTFSAudit`, `*-Item2`, inheritance cmdlets);
`Enable-/Disable-Privileges -PassThru` writes one collection;
`New-NTFSSymbolicLink -PassThru` returns `FileInfo` for folder links.
- (23) Typos: "Privliege" in the `Get-NTFSEffectiveAccess` warning; "are
now enabled" in the `Disable-Privileges` verbose message.
- (24) Dead code in `RemoveItem2.cs` and `OtherCmdlets.cs`.
- Found with group D: `-PassThru` of the `*-Item2` cmdlets wrote the item
also when `-WhatIf` skipped the operation.
#### E: Maintainer decisions (implemented on `ai/decisions-e`, not merged)
- D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL;
the page states the result and the risk, and a test pins it.
- D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets
(Decision 13; listed under `Changed`).
- D4: `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`, with
the `*AccessRules` names as aliases.
- D5: `Get-FileHash2` works in PowerShell 7; `RIPEMD160` and
`MACTripleDES` stop it there with `HashAlgorithmNotAvailable`.
`MACTripleDES` uses a random key (verified), so it is deprecated.
- Review of group E: the changelog now marks the `Set-NTFSInheritance`
change as breaking and warns that it leaves broader access in place.
#### Issue triage (fixes on `ai/issue-fixes`, not merged)
- Fixed: #3 (braces in a path), #86 (`$PWD` shadowed, also for the default
location of nine cmdlets, found by the review), #88 (an object passed by
position), #17 (an entry with `GenericAll`); `Docs/FAQ.md` answers the
recurring questions.
- Review of #17: generic rights are removed the way .NET removes other
rights, an exact match as it is, otherwise without `Synchronize`.
- Maintainer decisions of 2026-10-05: #5 (`Get-ChildItem2 -Attributes`
matches any listed attribute) and #82 (no `Size` alias) ship in 5.0.0 as
breaking changes. Their review added that an empty `-Attributes` value is
an error, as in `Get-ChildItem`.
- Open bugs: #34 and #67 (writes owner and group, rc3 if a file server is
available), #41 (drive root) and #90 (trailing space), both after 5.0.0.
1. The maintainer pushes the eight branches, opens the PRs, merges them in
order, and tags `5.0.0-rc2` once CI on `master` is green; then tests
rc2.
2. Release 5.0.0 through CI (Decision 12) after the tests: remove the
label, date `[Unreleased]` as `[5.0.0]`, tag `5.0.0` (steps in
`Docs/Contributing/05-Releasing.md`; CI warns when the changelog date
isn't the release day).
3. Repository settings: the hardening proposed on 2026-10-04 isn't applied
yet (checked 2026-10-05); the maintainer applies it before pushing the
stack. The details are with the maintainer, not in the repository.
4. Open bugs from the triage: #34 and #67 (the write includes owner and
group; `fix/#34` swallows every error) for rc3 if a file server to test
against is available; #41 (a drive root reads the device object) and #90
(a trailing space in a folder name) after 5.0.0. Enhancements: #22, #49,
#68, #77, #87.
5. `pwsh` 7.6.1 crashed three times during test runs on the ARM64
workstation (x64 emulation) with an access violation in `coreclr.dll` or
`System.Management.Automation.dll`, without module frames; not
reproducible on demand. Check whether CI on native x64 shows it.
6. Minor review findings that the PRs list but don't fix, for example
`Copy-Item2 -WhatIf` reporting a destination conflict as an error, and
relative path forms that the `*-Item2` cmdlets resolve themselves; the
maintainer tracks the useful ones as issues.
7. Optional for the maintainer: delete the AppVeyor project and revoke its
GitHub authorization, restrict wiki editing to collaborators, and ask
`Sup3rlativ3` to delete the Read the Docs project.

69
.memory-bank/systemPatterns.md

@ -1,6 +1,6 @@
---
status: current
last-verified: 2026-10-04
last-verified: 2026-10-05
owner: active-agent
source: repository evidence
---
@ -28,17 +28,18 @@ NTFSSecurity.dll ── cmdlets ──> Security2.dll (FileSystemAccessRule2,
── privileges ──> PrivilegeControl / ProcessPrivileges
```
- `BaseCmdlet` resolves relative paths against `$PWD`.
- `BaseCmdletWithPrivControl` (access, audit, inheritance, owner, security
descriptor, and privilege cmdlets) enables Backup, Restore, TakeOwnership,
and Security in `BeginProcessing` when `PrivateData.EnablePrivileges` is
- `BaseCmdlet` resolves only relative paths, against the current file
system location of the session (not `$PWD`, #86). Path parameters carry
`[FileSystemPathTransformation]`, which binds file objects as full paths.
- On access denied, most cmdlets retry through `InvokeAsOwner`, which takes
ownership and restores the previous owner on every exit path.
- `BaseCmdletWithPrivControl` enables Backup, Restore, TakeOwnership, and
Security in `BeginProcessing` when `PrivateData.EnablePrivileges` is
`$true`, and disables the ones it enabled in `EndProcessing`.
- `PrivateData` switches: `EnablePrivileges` (base cmdlet), `GetInheritedFrom`
(`Get-NTFSAccess`, `Get-NTFSAudit`), `GetFileSystemModeProperty` and
`IdentifyHardLinks` (`Get-ChildItem2`), `ShowAccountSid` (format file).
- Cmdlets accept either `-Path` (alias `FullName`) or `-SecurityDescriptor`
(from `Get-NTFSSecurityDescriptor`); SD sets change the in-memory object
until `Set-NTFSSecurityDescriptor` writes it back.
- `PrivateData` switches: `EnablePrivileges`, `GetInheritedFrom`,
`GetFileSystemModeProperty`, `IdentifyHardLinks`, `ShowAccountSid`.
- Cmdlets accept `-Path` (alias `FullName`) or `-SecurityDescriptor`; the
SD sets change the object in memory until `Set-NTFSSecurityDescriptor`.
## Decisions
@ -64,36 +65,28 @@ Each Decision record is a file in `decisions/`; read only the relevant ones.
### Verifying documentation
- Run platyPS in Windows PowerShell 5.1 against a module build; a copy of
- Run platyPS in Windows PowerShell 5.1 against a Release build; a copy of
`Docs/Cmdlets` must round-trip through `Update-MarkdownHelp` unchanged.
platyPS rewrites non-ASCII punctuation, so keep cmdlet pages ASCII-only.
It takes a parameter's `Position` from `Get-Help`, that is from the shipped
help file: after a position change, edit the page YAML, run
Keep cmdlet pages ASCII-only. platyPS takes `Position` and `Required` from
the shipped help file: after such a change, edit the page YAML, run
`New-ExternalHelp`, rebuild, and check the round trip.
- Links: MarkdownLinkCheck checks relative `Docs` links (no anchors),
`Tests\Wiki.Tests.ps1` the wiki links and anchors; neither covers
`README.md` and `CHANGELOG.md`. The wiki is generated from `Docs` (never
edit it); `Docs/README.md` becomes Home, its cmdlet groups the sidebar.
- In cmdlet pages, end a sentence with a link (platyPS drops the space after
it). Verify examples in a `$env:TEMP` sandbox, never on real data.
- MarkdownLinkCheck checks relative `Docs` links, `Tests\Wiki.Tests.ps1`
the wiki links and anchors. The wiki is generated from `Docs` (never edit
it); `Docs/README.md` becomes Home, its cmdlet groups the sidebar.
- In cmdlet pages, end a sentence with a link (platyPS drops the space
after it). Verify examples in a `$env:TEMP` sandbox, never on real data.
### Testing the module
- Pester 5 tests in `Tests/*.Tests.ps1` import
`NTFSSecurity\bin\Release\NTFSSecurity.psd1`; CI runs every file of
`Tests` in Windows PowerShell 5.1 and in PowerShell 7 (Decision 11) with
`.github/scripts/Invoke-Tests.ps1` (job summary, NUnit `test-results`).
- Pester 5 tests in `Tests/*.Tests.ps1` import the Release build; CI runs
them in Windows PowerShell 5.1 and PowerShell 7 (Decision 11).
- A test that changes files, links, or security descriptors uses
`Tests\TestHelpers.psm1`: its own sandbox below
`$env:TEMP\NTFSSecurity.Tests`, `Assert-TestSandboxPath` before each
change, `Remove-TestSandbox` (links first, then ACL reset). Cases that need
a privilege skip with `Test-PrivilegeHeld` and run in CI (elevated);
`Block-TestReadPermission` (OWNER RIGHTS deny) makes a read fail without
elevation.
- `Get-Help -Online` tests use the internal hook `BypassOnlineHelpRetrieval`,
which PowerShell 7 ignores for the help file: 36 tests run only in Windows
PowerShell.
- `Manifest.Tests.ps1`: `Test-ModuleManifest` clean, exactly 36 cmdlets, one
version in manifest and assemblies (Decision 10). `Release.Tests.ps1`:
release notes for the manifest version, and the packages (`FileList`
files, version with label, command tags, zip layout).
`Tests\TestHelpers.psm1`: its own sandbox, `Assert-TestSandboxPath`
before each change, `Remove-TestSandbox`. Cases that need a privilege
skip with `Test-PrivilegeHeld` and run in CI (elevated);
`Block-TestReadPermission` and `Block-TestWritePermission` make a read or
a write fail without elevation.
- `Get-Help -Online` tests run only in Windows PowerShell, which honors the
hook `BypassOnlineHelpRetrieval`. `Manifest.Tests.ps1` and
`Release.Tests.ps1` check the manifest, the version (Decision 10), the
release notes, and the packages.

5
.memory-bank/techContext.md

@ -65,8 +65,9 @@ source: repository evidence
## Constraints
- `ModuleVersion` on `master` is `5.0.0` with the prerelease label `rc1`;
5.0.0-rc1 is on the Gallery (published 2026-10-04 by CI). The latest
- `ModuleVersion` on `master` is `5.0.0` with the prerelease label `rc1`
(`ai/release-5.0.0-rc2` sets `rc2`); 5.0.0-rc1 is on the Gallery
(published 2026-10-04 by CI). The latest
stable tag and Gallery release is `4.2.6`. The manifest requires
PowerShell 5.1 and .NET Framework 4.5.2, uses `RootModule`, and lists
exactly 36 cmdlets; `Test-ModuleManifest` passes in Windows PowerShell 5.1

7
CHANGELOG.md

@ -36,6 +36,9 @@ The format is based on
into the documentation, and complete the version history with the release
dates from the PowerShell Gallery, the missing notes for 4.2.2, 4.2.5, and
4.2.6, and detailed notes for 4.2.4
- Describe the module as a PowerShell module in the manifest, which the
PowerShell Gallery shows; it said Windows PowerShell, although the module
supports PowerShell 7 as well
- Rename `-RemoveInheritedAccessRules` of `Disable-NTFSAuditInheritance` to
`-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of
`Enable-NTFSAuditInheritance` to `-RemoveExplicitAuditRules`, because they
@ -198,5 +201,9 @@ The format is based on
`GenericAll`, which Windows keeps in the inherit-only entries of folders;
it failed with "The value '269484032' is not valid"
([#17](https://github.com/raandree/NTFSSecurity/issues/17))
- Fix `Enable-NTFSAuditInheritance`, `Disable-NTFSAuditInheritance`, and
`Set-NTFSInheritance -AuditInheritanceEnabled`, which failed with "Access
is denied" for a file or folder without audit entries, also in an elevated
session with the Security privilege
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

4
Docs/Contributing/05-Releasing.md

@ -45,7 +45,9 @@ release notes for the version of the module manifest.
1. Set the version and the `Prerelease` label, such as `rc1`, and make sure
that the `[Unreleased]` section of `CHANGELOG.md` describes the changes.
Merge the change into `master`.
Add the version that the Gallery has now to `$publishedVersions` in
`Tests/Repository.Tests.ps1`, so that a test catches a version that is
reused. Merge the change into `master`.
2. Tag the commit on `master` with the version and push the tag:
```powershell

4
Docs/README.md

@ -38,8 +38,8 @@ Install the module from the
Install-Module -Name NTFSSecurity
```
To try a prerelease of the next version, such as `5.0.0-rc1`, add
`-AllowPrerelease`. A prerelease is for testing; don't use it in production.
To try a prerelease of the next version, add `-AllowPrerelease`. A
prerelease is for testing; don't use it in production.
You can also install a release without the PowerShell Gallery. Download
`NTFSSecurity.zip` from the

4
NTFSSecurity/NTFSSecurity.psd1

@ -11,7 +11,7 @@
Copyright = '2018'
Description = 'Windows PowerShell Module for managing file and folder security on NTFS volumes'
Description = 'PowerShell module for managing file and folder security on NTFS volumes'
PowerShellVersion = '5.1'
@ -102,7 +102,7 @@
ProjectUri = 'https://github.com/raandree/NTFSSecurity'
ReleaseNotes = 'https://github.com/raandree/NTFSSecurity/blob/master/CHANGELOG.md'
# Remove the prerelease label for the final release, see Docs/Contributing/05-Releasing.md
Prerelease = 'rc1'
Prerelease = 'rc2'
}
}
}

34
Security2/FileSystem/FileSystemInheritanceInfo.cs

@ -106,8 +106,37 @@ namespace Security2
#endregion GetFileSystemInheritanceInfo
#region Enable / DisableInheritance internal
// An item without audit entries can have no SACL at all. AlphaFS writes the flag that disables or enables
// audit inheritance only together with a SACL, and a write without any section is denied: (5) Access is
// denied. The empty SACL is added only to a descriptor that was read with its SACL, so that writing it can't
// remove audit entries.
private static void AddMissingSystemAcl(FileSystemSecurity2 sd)
{
if (!sd.HasAuditSection)
{
return;
}
var rawDescriptor = new RawSecurityDescriptor(sd.SecurityDescriptor.GetSecurityDescriptorBinaryForm(), 0);
if (rawDescriptor.SystemAcl != null)
{
return;
}
rawDescriptor.SystemAcl = new RawAcl(GenericAcl.AclRevision, 0);
rawDescriptor.SetFlags(rawDescriptor.ControlFlags | ControlFlags.SystemAclPresent);
var binaryForm = new byte[rawDescriptor.BinaryLength];
rawDescriptor.GetBinaryForm(binaryForm, 0);
sd.SecurityDescriptor.SetSecurityDescriptorBinaryForm(binaryForm, AccessControlSections.Audit);
}
private static void EnableInheritance(FileSystemSecurity2 sd, bool removeExplicitAccessRules, InheritanceScope scope)
{
if (scope == InheritanceScope.Audit)
{
AddMissingSystemAcl(sd);
}
if (sd.IsFile)
{
if (scope == InheritanceScope.Access)
@ -174,6 +203,11 @@ namespace Security2
private static void DisableInheritance(FileSystemSecurity2 sd, bool removeInheritedAccessRules, InheritanceScope scope)
{
if (scope == InheritanceScope.Audit)
{
AddMissingSystemAcl(sd);
}
if (sd.IsFile)
{
if (scope == InheritanceScope.Access)

53
Tests/Inheritance.Tests.ps1

@ -124,12 +124,18 @@ Describe 'Set-NTFSInheritance' {
}
# In memory, the kept entries stay marked as inherited; Windows stores them as explicit ones on write.
# The descriptor holds only the access entries. Windows marks the inherited entries of a DACL that isn't in
# the auto-inherit format, such as that of a file in the temp folder of the user, only when the SACL isn't
# read with it, and Get-NTFSSecurityDescriptor reads the SACL with the Security privilege.
It 'Should keep the inherited access entries of a security descriptor' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepDescriptor'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
$sd = Get-NTFSSecurityDescriptor -Path $file
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access
)
$sidType = [System.Security.Principal.SecurityIdentifier]
$inheritedCount = @($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)).Count
$inheritedCount | Should -BeGreaterThan 0
Set-NTFSInheritance -SecurityDescriptor $sd -AccessInheritanceEnabled $false
@ -141,10 +147,14 @@ Describe 'Set-NTFSInheritance' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepAudit'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights Delete -AuditFlags Failure
Disable-NTFSAuditInheritance -Path $file
Disable-NTFSAuditInheritance -Path $file -ErrorVariable disableErrors -ErrorAction SilentlyContinue
$disableErrors | Should -BeNullOrEmpty
(Get-NTFSInheritance -Path $file).AuditInheritanceEnabled | Should -BeFalse
Set-NTFSInheritance -Path $file -AuditInheritanceEnabled $true
Set-NTFSInheritance -Path $file -AuditInheritanceEnabled $true -ErrorVariable inheritanceErrors -ErrorAction SilentlyContinue
$inheritanceErrors | Should -BeNullOrEmpty
(Get-NTFSInheritance -Path $file).AuditInheritanceEnabled | Should -BeTrue
@(Get-NTFSAudit -Path $file -ExcludeInherited) | Should -HaveCount 1
}
}
@ -190,6 +200,43 @@ Describe 'Set-NTFSInheritance' {
}
}
Describe 'Audit inheritance of an item without audit entries' {
# A new item has no SACL. Before 5.0.0, the cmdlets changed only the flag that disables or enables audit
# inheritance, which is written only together with a SACL, so they wrote no section at all: (5) Access is denied.
It '<Command> should set the audit inheritance of a <Type> and keep its access entries' -Skip:(-not $canChangeAudit) -ForEach @(
@{ Command = 'Disable-NTFSAuditInheritance'; Parameters = @{}; Type = 'file'; Expected = $false }
@{ Command = 'Disable-NTFSAuditInheritance'; Parameters = @{}; Type = 'folder'; Expected = $false }
@{ Command = 'Enable-NTFSAuditInheritance'; Parameters = @{}; Type = 'file'; Expected = $true }
@{ Command = 'Enable-NTFSAuditInheritance'; Parameters = @{}; Type = 'folder'; Expected = $true }
@{ Command = 'Set-NTFSInheritance'; Parameters = @{ AuditInheritanceEnabled = $false }; Type = 'folder'; Expected = $false }
) {
$path = New-TestSandboxItem -Sandbox $sandbox -Name 'NoAudit' -Directory:($Type -eq 'folder')
Assert-TestSandboxPath -Sandbox $sandbox -Path $path
@((Get-Acl -LiteralPath $path -Audit).Audit) | Should -BeNullOrEmpty
$accessEntries = (Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access')
& $Command -Path $path @Parameters -ErrorVariable inheritanceErrors -ErrorAction SilentlyContinue
$inheritanceErrors | Should -BeNullOrEmpty
(Get-NTFSInheritance -Path $path).AuditInheritanceEnabled | Should -Be $Expected
(Get-Acl -LiteralPath $path).GetSecurityDescriptorSddlForm('Access') | Should -Be $accessEntries
}
# Written later, an added empty SACL would replace the audit entries of the item.
It 'Should add no SACL to a security descriptor that was read without its audit entries' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'NoAuditSection'
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access
)
Disable-NTFSAuditInheritance -SecurityDescriptor $sd
$binaryForm = $sd.SecurityDescriptor.GetSecurityDescriptorBinaryForm()
$descriptor = New-Object -TypeName 'System.Security.AccessControl.RawSecurityDescriptor' -ArgumentList $binaryForm, 0
$null -eq $descriptor.SystemAcl | Should -BeTrue
}
}
Describe 'Audit inheritance switches' {
# Before 5.0.0, the switches were named after access entries, although they remove audit entries.
It '<Command> should take -<Name> with the alias -<Alias>' -ForEach @(

32
Tests/Repository.Tests.ps1

@ -1,6 +1,7 @@
<#
Tests repository files that the build and GitHub use, without a build: every packages.config lists the AlphaFS
version that the projects reference and ship, and Dependabot keeps the actions of the CI workflow up to date.
version that the projects reference and ship, Dependabot keeps the actions of the CI workflow up to date, and the
manifest and the README describe the release.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.'
@ -75,3 +76,32 @@ Describe 'Dependabot configuration' {
$raw | Should -Match '(?m)^\s+groups:\s*\n\s+[\w-]+:\s*\n\s+patterns:\s*\n\s+-\s*["'']\*["'']\s*$'
}
}
Describe 'Release metadata' {
BeforeAll {
$repositoryPath = Join-Path -Path $PSScriptRoot -ChildPath '..'
$manifest = Import-PowerShellDataFile -Path (Join-Path -Path $repositoryPath -ChildPath 'NTFSSecurity\NTFSSecurity.psd1')
$version = $manifest.ModuleVersion
if ($manifest.PrivateData.PSData.Prerelease) {
$version = '{0}-{1}' -f $version, $manifest.PrivateData.PSData.Prerelease
}
}
# Before 5.0.0-rc2, the description said "Windows PowerShell Module", although the module supports PowerShell 7.
It 'Should have the description that the PowerShell Gallery shows for the module' {
$manifest.Description | Should -BeExactly 'PowerShell module for managing file and folder security on NTFS volumes'
}
# The PowerShell Gallery doesn't accept a version twice. Add every published version to this list
# (Docs/Contributing/05-Releasing.md).
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'
$publishedVersions | Should -Not -Contain $version
}
It 'Should not name a prerelease version in the README, which outlives the release' {
Get-Content -LiteralPath (Join-Path -Path $repositoryPath -ChildPath 'Docs\README.md') -Raw |
Should -Not -Match '\d+\.\d+\.\d+-[A-Za-z]'
}
}

Loading…
Cancel
Save