--- status: current last-verified: 2026-10-08 owner: active-agent source: repository evidence --- # System patterns ## Architecture ```text NTFSSecurity.psd1 ─┬─ ScriptsToProcess: NTFSSecurity.Init.ps1 │ Add-Type: Security2.dll, PrivilegeControl.dll, │ ProcessPrivileges.dll, inline NTFS.DriveInfoExt; │ Update-FormatData -PrependPath format.ps1xml ├─ TypesToProcess: NTFSSecurity.types.ps1xml │ (Owner, IsInheritanceBlocked, LengthOnDisk on │ FileInfo/DirectoryInfo; AccountType on ACEs) ├─ RootModule: NTFSSecurity.psm1 (aliases) ├─ NestedModules: NTFSSecurity.dll (36 cmdlets) └─ en-US\NTFSSecurity.dll-Help.xml (Get-Help; generated from Docs/Cmdlets, Decision 8) NTFSSecurity.dll ── cmdlets ──> Security2.dll (FileSystemAccessRule2, FileSystemAuditRule2, IdentityReference2, FileSystemInheritanceInfo, EffectiveAccess) ── long paths ──> AlphaFS ── privileges ──> PrivilegeControl / ProcessPrivileges ``` - `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` and, since 5.0.0-rc6, in `Dispose`: PowerShell skips `EndProcessing` when a later command, such as `Select-Object -First`, or a terminating error stops the pipeline, but calls `Dispose`. `Enable-Privileges` keeps them (`KeepEnabledPrivileges`). The cleanup reads the current state of each privilege, because another command in the pipeline can have changed it, and tries every privilege even when one fails: `EndProcessing` warns, `Dispose` stays silent, because PowerShell ignores exceptions thrown there and no stream is open anymore. - `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 Each Decision record is a file in `decisions/`; read only the relevant ones. | # | Decision | | --- | --- | | 1 | [Use the canonical Memory Bank base](decisions/0001-canonical-memory-bank.md) | | 2 | [Cmdlet reference stays platyPS markdown](decisions/0002-platyps-cmdlet-reference.md) | | 3 | [Document the source at HEAD](decisions/0003-document-source-at-head.md) | | 4 | [Online help points to GitHub](decisions/0004-online-help-on-github.md) | | 5 | [Document defects, don't fix them in docs work](decisions/0005-document-defects-separately.md) | | 6 | [CI checks the docs against a build of the source](decisions/0006-ci-checks-docs-against-build.md) | | 7 | [CHANGELOG lists user-visible changes only](decisions/0007-changelog-user-visible-only.md) | | 8 | [Commit the generated help file and check it in CI](decisions/0008-commit-generated-help.md) | | 9 | [Keep the documentation on GitHub](decisions/0009-docs-on-github.md) | | 10 | [One version for the manifest, assemblies, and changelog](decisions/0010-one-version.md) | | 11 | [CI and the wiki run on GitHub Actions](decisions/0011-github-actions.md) | | 12 | [Releases are built and published by CI on a version tag](decisions/0012-ci-releases.md) | | 13 | [Set-NTFSInheritance keeps entries like the dedicated cmdlets](decisions/0013-set-inheritance-keeps-entries.md) | | 14 | [Repository hardening is optional](decisions/0014-repository-hardening-optional.md) | | 15 | [Merge stacked pull requests in order with merge commits](decisions/0015-merge-stacks-with-merge-commits.md) | | 16 | [Fix only reproducible bugs](decisions/0016-fix-reproducible-bugs-only.md) | | 17 | [Issue labels](decisions/0017-issue-labels.md) | | 18 | [NTFSSecurity will be archived](decisions/0018-archive-for-windowsaccesscontrol.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) | | 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 ### 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 - Run platyPS in Windows PowerShell 5.1 against a Release build; a copy of `Docs/Cmdlets` must round-trip through `Update-MarkdownHelp` unchanged. 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. - 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 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, `Assert-TestSandboxPath` before each change, `Remove-TestSandbox`. Cases that need a privilege skip with `Test-PrivilegeHeld`, cases that need its absence skip when elevated; CI runs the suite elevated and as a basic user in both editions, so each case runs somewhere. `Block-Test*` make a read or a write fail without elevation; `Set-TestOwner` with `EnablePrivileges = $false` reproduces an owner the user can't assign. - Fixtures write a DACL with `SetAccessControl`, never with `Set-Acl`: `Set-Acl` compares `AreAuditRulesProtected` of the new descriptor with `AreAccessRulesProtected` of the item (`FileSystemSecurity.cs` of PowerShell), so for an item with a protected DACL it writes the audit section too. Without the Security privilege that fails with `PrivilegeNotHeldException`; with it, `Set-Acl` writes every section and drops the audit entries. Windows PowerShell has `FileInfo`/`DirectoryInfo.SetAccessControl`; PowerShell 7 has `[System.IO.FileSystemAclExtensions]::SetAccessControl`. - `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. - The live tests in `Tests\Lab` (Decision 20) run as domain accounts in a lab: on the client over SMB, then on the file server, which checks what the client runs left. They read and write descriptors as Windows stores them with `GetFileSecurity` and `SetFileSecurity`, because `GetNamedSecurityInfo` converts a DACL without the auto-inherit flag and returns its owner. The expected effective rights come from the S4U tokens of the file server and the client, like the Effective Access tab.