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 status: current
last-verified: 2026-10-04 last-verified: 2026-10-05
owner: active-agent owner: active-agent
source: current task evidence source: current task evidence
--- ---
@ -9,62 +9,47 @@ source: current task evidence
## Current focus ## Current focus
Overnight run 2026-10-04/05 (autopilot, maintainer asleep): fix the code The overnight run of 2026-10-04/05 is finished. 5.0.0-rc2 waits on eight
defects A to D of `progress.md`, implement the maintainer's E decisions, stacked local branches (see `progress.md`): the maintainer pushes them,
triage the 37 open issues, and prepare 5.0.0-rc2. Eight stacked local opens one PR each against `master`, merges them in order with merge
branches, each a PR against `master`, to be merged in this order with merge commits, and tags `5.0.0-rc2` on the last merge commit once CI on `master`
commits: `ai/maintenance`, `ai/defects-a`, `ai/defects-b`, `ai/defects-c`, is green. The run's report lists the commands, the PR descriptions, a
`ai/defects-d`, `ai/decisions-e`, `ai/issue-fixes`, reply for each issue, and the open questions.
`ai/release-5.0.0-rc2`. Nothing is pushed; the maintainer pushes, opens
the PRs, and tags `5.0.0-rc2` after the merges.
## Maintainer decisions for the run (2026-10-04) ## Maintainer decisions for the run (2026-10-04)
- D1: the fixes ship in 5.0.0; entries go under `[Unreleased]` (`Fixed`; - D1: the fixes ship in 5.0.0, under `[Unreleased]`; the behavior changes
intended behavior changes under `Changed` with the way back). The last of D3 and D4 are listed under `Changed` with the way back.
PR sets `Prerelease = 'rc2'`. - D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL.
- D2: `Clear-NTFSAccess -DisableInheritance` keeps leaving an empty DACL; - D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets
the page states the result and the risk. (Decision 13).
- D3: `Set-NTFSInheritance` keeps entries like the dedicated cmdlets.
- D4: `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`, with - D4: `-RemoveInheritedAuditRules` and `-RemoveExplicitAuditRules`, with
the `*AccessRules` names as aliases. the old names as aliases.
- D5: `Get-FileHash2` works in PowerShell 7 for every algorithm .NET has; - D5: `Get-FileHash2` works in PowerShell 7; `MACTripleDES` is deprecated.
a missing one fails only when requested. - D6: only reproducible bugs are fixed; other behavior changes are
- D7: Dependabot for `github-actions` only; AlphaFS 2.2.1 in questions for the maintainer.
`NTFSSecurity\packages.config` (no upgrade, no changelog entry). - 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 ## Evidence
- Baseline at `e0f5366` (Release build, workstation): Windows PowerShell - Every branch tip: Release build without new warnings (296 at the top,
261 passed, 7 skipped; PowerShell 7 232 passed, 36 skipped (268 tests). 305 at the baseline), docs checks clean, package dry run passed.
- `ai/maintenance` adds `Tests\Repository.Tests.ps1` (8 tests): Windows - Tests at the top branch: Windows PowerShell 423 passed, 26 skipped;
PowerShell 269 passed, 7 skipped; PowerShell 7 240 passed, 36 skipped. PowerShell 7 394 passed, 55 skipped (449). The baseline had 268 tests.
Review: Dependabot PRs ran unreviewed actions in a job with - Tests that need privileges skip on the workstation and run in CI only;
`contents: write`; the wiki preview is now read-only (`publish-wiki`). the PR descriptions list them.
- `ai/defects-a` fixes defects 1 to 13 and the same repeat bug in - Reviews: one security review per PR; the Major findings were fixed in
`Get-NTFSAccess` (found with 4); its review fixes are in the last the PR that had them (PR 1: 1, PR 2: 5, PR 4: 2, PR 6: 1, PR 7: 3,
commit: Windows PowerShell 312 passed, 17 skipped; PowerShell 7 282 PR 8: 2).
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).
## Next step ## 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 status: current
last-verified: 2026-10-04 last-verified: 2026-10-05
owner: active-agent owner: active-agent
source: repository evidence source: repository evidence
--- ---
@ -9,188 +9,76 @@ source: repository evidence
## Current status ## Current status
PRs #91 to #98 are merged. CI published the prerelease 5.0.0-rc1 from 5.0.0-rc2 is ready on eight stacked local branches from the overnight run of
`master` (`e0f5366`, tag `5.0.0-rc1`) to the PowerShell Gallery and GitHub; 2026-10-04/05; none is pushed. Each is a PR against `master`, merged in
the stable Gallery version is still 4.2.6. CI runs on GitHub Actions: this order with merge commits: `ai/maintenance`, `ai/defects-a`,
build, docs checks, tests in Windows PowerShell 5.1 and PowerShell 7, `ai/defects-b`, `ai/defects-c`, `ai/defects-d`, `ai/decisions-e`,
packages, the wiki generated from `Docs` (43 pages), and releases on a `ai/issue-fixes`, `ai/release-5.0.0-rc2`. The tag `5.0.0-rc2` on the last
version tag (Decision 12). Next: the maintainer tests 5.0.0-rc1. 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 ## Recent milestones
- 2026-10-02 to 2026-10-04: #91 aligned the docs with the code (Decision - 2026-10-02 to 2026-10-04: #91 to #97 aligned the docs with the code,
6; #83 closed as superseded), #92 did housekeeping (Decision 7), and shipped the help file, kept the docs on GitHub, set version 5.0.0, moved
#93 shipped the generated help file (Decision 8, `Tests\Help.Tests.ps1`). CI and a wiki generated from `Docs` to GitHub Actions, and completed the
- 2026-10-04: #94 to #96 dropped Read the Docs (Decision 9), set version version history (Decisions 6 to 11).
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-04: #98 (`e0f5366`) added releases on a version tag through CI - 2026-10-04: #98 (`e0f5366`) added releases on a version tag through CI
(Decision 12), with a prerelease first; `New-ModulePackage.ps1` adds the (Decision 12). The tag `5.0.0-rc1` published to the Gallery and created
command tags that PSResourceGet drops. The tag `5.0.0-rc1` (run the GitHub prerelease; the installed module passed the full suite.
37230802387) published to the Gallery at 20:12 UTC and created the - 2026-10-05, overnight run (local branches): the 24 code defects of work
GitHub prerelease. Verified: the Gallery nupkg and the GitHub zip are package 5 fixed with regression tests, plus what the reviews found: a
byte-identical to the CI artifacts, the DLLs are optimized Release failed retry after taking ownership left the owner changed, and
builds, the Gallery shows the prerelease flag, the release notes link, `-PassThru` wrote objects after a failed change or under `-WhatIf`.
and all 36 `PSCmdlet_` and `PSCommand_` tags, and the installed module Maintainer decisions D2 to D5 (Decision 13), the issues #3, #4, #17, #74,
passes the full suite (Windows PowerShell 261 passed, 7 skipped; #86, and #88 fixed, the 37 open issues triaged, `Docs/FAQ.md`, Dependabot for
PowerShell 7 232 passed, 36 skipped). 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 ## Stable capabilities
- 36 cmdlets: access (7), audit (5), inheritance (6), owner and security - 36 cmdlets: access (7), audit (5), inheritance (6), owner and security
descriptor (4), privileges (3), long-path items (6), links, hash, and descriptor (4), privileges (3), long-path items (6), links, hash, and
disk space (5). disk space (5).
- Works in Windows PowerShell 5.1 and PowerShell 7 (smoke-tested), except - Works in Windows PowerShell 5.1 and PowerShell 7. In PowerShell 7,
`Get-FileHash2`, which fails in PowerShell 7 (missing `RIPEMD160` type). `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 ## Open work
Work packages in the order agreed with the maintainer. Each gets one 1. The maintainer pushes the eight branches, opens the PRs, merges them in
`ai/<slug>` branch and PR, committed locally. The maintainer pushes and order, and tags `5.0.0-rc2` once CI on `master` is green; then tests
opens the PR (the agent can't; see `techContext.md`, Constraints), and the rc2.
next package starts only after the maintainer's go-ahead. 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
Items 1 to 4c are done: housekeeping (#92), shipped help (#93), docs on `Docs/Contributing/05-Releasing.md`; CI warns when the changelog date
GitHub (#94), manifest and version 5.0.0 (#95), CI and the wiki on GitHub isn't the release day).
Actions (#96), version history from the Gallery (#97). Optional for the 3. Repository settings: the hardening proposed on 2026-10-04 isn't applied
maintainer: delete the AppVeyor project and revoke its GitHub yet (checked 2026-10-05); the maintainer applies it before pushing the
authorization, restrict wiki editing to collaborators, and ask stack. The details are with the maintainer, not in the repository.
`Sup3rlativ3` to delete the Read the Docs project. 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
4d. Release 5.0.0 through CI (Decision 12): 5.0.0-rc1 published and against is available; #41 (a drive root reads the device object) and #90
verified (#98); the maintainer tests it. The final release PR comes on (a trailing space in a folder name) after 5.0.0. Enhancements: #22, #49,
release day (CI warns when the changelog date isn't that day): remove #68, #77, #87.
the label, date `[Unreleased]` as `[5.0.0]`, tag `5.0.0` (steps in 5. `pwsh` 7.6.1 crashed three times during test runs on the ARM64
`Docs/Contributing/05-Releasing.md`). Also consider the manifest workstation (x64 emulation) with an access violation in `coreclr.dll` or
`Description` ("Windows PowerShell Module") and the `5.0.0-rc1` example `System.Management.Automation.dll`, without module frames; not
in `Docs/README.md`. Releases no longer come from a local Debug build. reproducible on demand. Check whether CI on native x64 shows it.
4e. Repository settings, proposed to the maintainer on 2026-10-04 (not yet 6. Minor review findings that the PRs list but don't fix, for example
agreed): the `powershell-gallery` environment has no protection rules `Copy-Item2 -WhatIf` reporting a destination conflict as an error, and
and no deployment policy, so a workflow on any branch can use relative path forms that the `*-Item2` cmdlets resolve themselves; the
`PSGALLERY_API_KEY` (`nyanhp` also has write access); `master` has no maintainer tracks the useful ones as issues.
protection or ruleset; head branches aren't deleted on merge; the 7. Optional for the maintainer: delete the AppVeyor project and revoke its
remote branches `fix/#34` and `test/transfer` (2023-11-28, two commits GitHub authorization, restrict wiki editing to collaborators, and ask
each) aren't merged. Dependabot for the SHA-pinned actions comes with `Sup3rlativ3` to delete the Read the Docs project.
`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.

69
.memory-bank/systemPatterns.md

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

5
.memory-bank/techContext.md

@ -65,8 +65,9 @@ source: repository evidence
## Constraints ## Constraints
- `ModuleVersion` on `master` is `5.0.0` with the prerelease label `rc1`; - `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 (`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 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 PowerShell 5.1 and .NET Framework 4.5.2, uses `RootModule`, and lists
exactly 36 cmdlets; `Test-ModuleManifest` passes in Windows PowerShell 5.1 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 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 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 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 - Rename `-RemoveInheritedAccessRules` of `Disable-NTFSAuditInheritance` to
`-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of `-RemoveInheritedAuditRules` and `-RemoveExplicitAccessRules` of
`Enable-NTFSAuditInheritance` to `-RemoveExplicitAuditRules`, because they `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; `GenericAll`, which Windows keeps in the inherit-only entries of folders;
it failed with "The value '269484032' is not valid" it failed with "The value '269484032' is not valid"
([#17](https://github.com/raandree/NTFSSecurity/issues/17)) ([#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 [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 1. Set the version and the `Prerelease` label, such as `rc1`, and make sure
that the `[Unreleased]` section of `CHANGELOG.md` describes the changes. 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: 2. Tag the commit on `master` with the version and push the tag:
```powershell ```powershell

4
Docs/README.md

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

4
NTFSSecurity/NTFSSecurity.psd1

@ -11,7 +11,7 @@
Copyright = '2018' 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' PowerShellVersion = '5.1'
@ -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 = 'rc1' Prerelease = 'rc2'
} }
} }
} }

34
Security2/FileSystem/FileSystemInheritanceInfo.cs

@ -106,8 +106,37 @@ namespace Security2
#endregion GetFileSystemInheritanceInfo #endregion GetFileSystemInheritanceInfo
#region Enable / DisableInheritance internal #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) private static void EnableInheritance(FileSystemSecurity2 sd, bool removeExplicitAccessRules, InheritanceScope scope)
{ {
if (scope == InheritanceScope.Audit)
{
AddMissingSystemAcl(sd);
}
if (sd.IsFile) if (sd.IsFile)
{ {
if (scope == InheritanceScope.Access) if (scope == InheritanceScope.Access)
@ -174,6 +203,11 @@ namespace Security2
private static void DisableInheritance(FileSystemSecurity2 sd, bool removeInheritedAccessRules, InheritanceScope scope) private static void DisableInheritance(FileSystemSecurity2 sd, bool removeInheritedAccessRules, InheritanceScope scope)
{ {
if (scope == InheritanceScope.Audit)
{
AddMissingSystemAcl(sd);
}
if (sd.IsFile) if (sd.IsFile)
{ {
if (scope == InheritanceScope.Access) 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. # 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' { It 'Should keep the inherited access entries of a security descriptor' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepDescriptor' $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepDescriptor'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file 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] $sidType = [System.Security.Principal.SecurityIdentifier]
$inheritedCount = @($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)).Count $inheritedCount = @($sd.SecurityDescriptor.GetAccessRules($false, $true, $sidType)).Count
$inheritedCount | Should -BeGreaterThan 0
Set-NTFSInheritance -SecurityDescriptor $sd -AccessInheritanceEnabled $false Set-NTFSInheritance -SecurityDescriptor $sd -AccessInheritanceEnabled $false
@ -141,10 +147,14 @@ Describe 'Set-NTFSInheritance' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepAudit' $file = New-TestSandboxItem -Sandbox $sandbox -Name 'KeepAudit'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Add-NTFSAudit -Path $file -Account 'Everyone' -AccessRights Delete -AuditFlags Failure 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 @(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' { Describe 'Audit inheritance switches' {
# Before 5.0.0, the switches were named after access entries, although they remove audit entries. # 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 @( 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 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( [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' '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*$' $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