Browse Source

Merge pull request #119 from raandree/ai/quality-gate-lab-matrix

test(lab): operating-system matrix, three module fixes, and first-lab acceptance for the 5.0.0 gate
pull/121/head 5.0.0-rc7
Raimund Andrée 1 day ago
committed by GitHub
parent
commit
fa0701b591
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 132
      .memory-bank/activeContext.md
  2. 62
      .memory-bank/decisions/0022-phase-2-behavior-changes.md
  3. 74
      .memory-bank/decisions/0023-non-windows-file-servers.md
  4. 150
      .memory-bank/decisions/0024-os-matrix-lab.md
  5. 171
      .memory-bank/deployment-notes.md
  6. 66
      .memory-bank/progress.md
  7. 19
      .memory-bank/systemPatterns.md
  8. 69
      .memory-bank/techContext.md
  9. 22
      CHANGELOG.md
  10. 6
      Docs/Cmdlets/Get-NTFSEffectiveAccess.md
  11. 9
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  12. 8
      Security2/FileSystem/FileSystemSecurity2.cs
  13. 51
      Security2/Win32/Lib.cs
  14. 31
      Tests/Inheritance.Tests.ps1
  15. 157
      Tests/Lab/Acceptance-2026-10-09-quality-gate-paths-Results.csv
  16. 251
      Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md
  17. 129
      Tests/Lab/Acceptance-2026-10-10-os-matrix-Cells.csv
  18. 297
      Tests/Lab/Acceptance-2026-10-10-os-matrix-Failures.csv
  19. 9
      Tests/Lab/Acceptance-2026-10-10-os-matrix-FirstLab.csv
  20. 57
      Tests/Lab/Acceptance-2026-10-10-os-matrix-LocalSuite.csv
  21. 44
      Tests/Lab/Acceptance-2026-10-10-os-matrix-Timeline.csv
  22. 760
      Tests/Lab/Acceptance-2026-10-10-os-matrix.md
  23. 125
      Tests/Lab/Acceptance/Add-OsMatrixMachine.ps1
  24. 91
      Tests/Lab/Acceptance/Complete-OsMatrixLab.ps1
  25. 105
      Tests/Lab/Acceptance/Deploy-OsMatrixLab.ps1
  26. 131
      Tests/Lab/Acceptance/Export-CellTimeline.ps1
  27. 101
      Tests/Lab/Acceptance/Export-MatrixResults.ps1
  28. 93
      Tests/Lab/Acceptance/Invoke-EffectiveAccessProbe.ps1
  29. 66
      Tests/Lab/Acceptance/Invoke-LocalSuite.ps1
  30. 151
      Tests/Lab/Acceptance/Probe-AccountRecreation.ps1
  31. 278
      Tests/Lab/Acceptance/Probe-EffectiveAccess.ps1
  32. 224
      Tests/Lab/Acceptance/Probe-LaterCommand.ps1
  33. 105
      Tests/Lab/Acceptance/Repair-OsMatrixBoot.ps1
  34. 265
      Tests/Lab/Acceptance/Run-MatrixLocalSuite.ps1
  35. 105
      Tests/Lab/Acceptance/Run-MatrixSequence.ps1
  36. 190
      Tests/Lab/Acceptance/Test-MatrixCleanup.ps1
  37. 79
      Tests/Lab/Acceptance/Test-MatrixReadiness.ps1
  38. 111
      Tests/Lab/Acceptance/Test-PublishedRelease.ps1
  39. 172
      Tests/Lab/Acceptance/Test-StaleAuthzModel.ps1
  40. 68
      Tests/Lab/Acceptance/Validate-LabResults.ps1
  41. 137
      Tests/Lab/Invoke-NTFSSecurityLabTest.ps1
  42. 623
      Tests/Lab/NTFSSecurity.Live.Tests.ps1
  43. 143
      Tests/Lab/Non-Windows-File-Server-Test.md
  44. 119
      Tests/Lab/README.md

132
.memory-bank/activeContext.md

@ -1,6 +1,6 @@
---
status: current
last-verified: 2026-10-09
last-verified: 2026-10-10
owner: active-agent
source: current task evidence
---
@ -9,27 +9,51 @@ source: current task evidence
## Current focus
Handoff 1 of the quality gate is finished locally on `ai/quality-gate-paths`,
from the reviewed head `f11ff41` of #117: 28 commits, nothing pushed. Both
stacked PRs stay open and green; rc6 remains the latest published
candidate and 4.2.6 the stable Gallery version. Every C# method that no test
The maintainer asked on 2026-10-09 at 21:21 UTC to continue with the release-gate
handoffs and to decide and report later. On 2026-10-10 at 09:10 UTC he merged
#116 (rc7, merge commit `8a6be9f`; rc7 is neither tagged nor published). His
`--delete-branch` also deleted the base branch of #117, so GitHub closed #117
unmerged; nothing is lost (`ai/quality-gate-coverage` is intact at `f11ff41`,
and the merge into `master` is conflict-free by simulation), and a new pull
request replaces it (Next step 1). Handoff 1 (paths) is draft #118 (`83149ee`,
CI green, base `ai/quality-gate-coverage`; rc6 is the latest published
candidate, 4.2.6 the stable Gallery version). Handoff 2 (operating-system
matrix) is draft #119 (`49734ef`, CI green, base `ai/quality-gate-paths`): the
lab `NtfsSecurityOsMatrixLab`, three fixes of the module in two commits
that the matrix found (`962887a`, `fdd7a8b`), the kit, the controller changes, and the
record `Tests/Lab/Acceptance-2026-10-10-os-matrix.md` (Decision 24, proposed).
The final local candidate `fdd7a8b` passes the module's suite on five operating
systems and the host (24 runs, no failure) and the live controller in three
cells of the matrix (1,374 passed, 0 failed, 12 skipped) and in the first lab,
where case 9 runs (245 passed, 0 failed, 1 skipped per edition). Handoff 3: Decision 22 was confirmed
under the delegation and stays proposed; nothing is published. Handoff 4:
Decision 23 (the #34 dossier); the risk acceptance is the maintainer's. The
agent's decisions of the night are D1 to D46 in
`decisions-night-2026-10-09.md` of the session files. Stable 5.0.0 stays gated.
The earlier state of handoff 1, from the reviewed head `f11ff41` of #117: 28
commits, which the maintainer pushed as draft #118. Every C# method that no test
visits is classified (223 explained, 8 open for the maintainer), and the
other paths have behavior tests. Eleven defects were fixed, ten of them
with a regression guard that is red before the fix and green after it (owner
restore, `InheritedFrom`, a later command that ends the pipeline or throws,
also at the error, verbose, and debug streams, `-Filter` brackets, null, and
`*.*`, public object APIs, a privilege left enabled); the leak of a native
buffer has no observable guard. Gate 3 must repeat the affected lab acceptance
before the next candidate is published. Decisions 21/22 and stable 5.0.0
remain gated; Decision 22 is proposed, not accepted.
buffer has no observable guard. The lab acceptance of those fixes was repeated
on 2026-10-09 (below); the published package still needs its own acceptance
in gate 3. Decisions 21/22 and stable 5.0.0
remain gated; Decision 22 is proposed: the agent confirmed all ten choices
on 2026-10-09 under the maintainer's delegation, and his own confirmation is
open.
## Evidence
- rc6 Release run `37839669028`, attempt 2, succeeded; GitHub prerelease
with zip appeared 2026-10-09 07:01:34 UTC. First attempt proves HTTP 409
after Gallery publication, not the previously assumed retry chronology.
- #116 is open, base master, head `d25647d`, CI build/wiki passed. rc7
publication is pending. The follow-up does not change that PR's head.
- #116 was merged into `master` on 2026-10-10 at 09:10:12Z (merge commit
`8a6be9f`, head `d25647d`); rc7 isn't tagged or published. #117 was closed
unmerged at 09:10:16Z (events `base_ref_deleted`, `closed`).
- Local changes: deletion/ownership guards (`a97e46f`); all scopes and
inheritance (`e7ee203`); absolute basic-user results (`51dec86`);
exact-package publication recovery (`95b827e`); first-hidden-item fix
@ -86,23 +110,85 @@ remain gated; Decision 22 is proposed, not accepted.
- Six checkpoints exist but report Standard, even after a successful
temporary ProductionOnly probe; policy restored, no restore performed.
Do not claim verified Production rollback evidence.
- Lab acceptance of the paths fixes, 20:41 to 21:42 UTC on 2026-10-09: the
candidate `83149ee` and its base `f11ff41` ran the same 244 tests per
edition (78 new, case 10) from their extracted packages. Candidate 486
passed, 0 failed, 2 expected skips; baseline 338 passed, 148 failed, each
green on the candidate; both editions gave the same counts. Fixture removal
verified by a separate read-only check in four domains and on both file
machines; six checkpoints (Standard type, no restore). Record, results CSV,
and limits: `Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md`. This
covers the gate-3 handoff table of the path report, except the published
package and the other operating systems.
- Wider matrix not deployed: 13 Server 2025 VMs; Windows 11/2019/2022
media present, OS detection cache empty. #34 has no reply since Oct 6.
- Full evidence: session artifact `quality-gate-3442194-20261009`;
repository report `Tests/Lab/Acceptance-2026-10-09-quality-gate.md`.
- Operating-system matrix, 2026-10-10 (record `Tests/Lab/Acceptance-2026-10-10-os-matrix.md`
with CSV tables): the suite of the final candidate `fdd7a8b` on OSFile19,
OSFile22, OSFile25, OSWin11E, OSWin11, and the host, four configurations each,
zero failures, skipped tests identical to the host's; the baseline `83149ee`
(run on OSFile22 and OSFile25) fails 4 elevated and 20 basic-user tests. Live: run
`rc7l`, three cells, 1,374 passed, 0 failed, 12 skipped. First lab (run `fl1`,
case 9 included, both editions): 245 passed, 0 failed, 1 skipped per edition,
fixture removed and verified clean. The Admin-role
effective-access failures of the earlier cells were not the module: in a replay
(`ab0` to `ab6`) the baseline failed two of three cells and the final candidate
one of three (not counting the warm-up `ab0`), and one model (the remote
authorization managers answer for an account name for about ten minutes after
the account was created again) fits all 43 Admin-role runs of 27 cells; the
Windows mechanism is unknown. The controller now names the account of case 3
anew for each fixture (`1dec389`); four more cells with it (`ab7` to `ab10`)
passed, two of them where the model predicts a failure for a reused name.
Reviewed by the built-in code-review agent (custom `security-reviewer`
unavailable): approve with Minor, fixed; a second review found one Major
(record accuracy), addressed by the replay; a follow-up review found no
Blocker or Major and five Minors, corrected; a second follow-up review found no
Blocker or Major and four Minors, corrected. The built-in `security-review`
agent (the custom reviewer is still unavailable) found no exploitable
vulnerability in the module changes and two LOW items that are not changed
(Next step 6). A dry run of `Run-MatrixSequence.ps1 -Version 5.0.0-rc6` on
OSFile19 showed that the published-package path works. State of the labs at
07:50 UTC on 2026-10-10: no fixture and no probe residue in either lab
(`Verify` of the matrix lab 07:45, of the first lab 07:29); the six VMs of the
matrix run, and `OSWin11E` (restarted 07:36) shuts itself down about an hour
after its start.
## Next step
1. The maintainer reviews and integrates `ai/quality-gate-paths` (stacked on
#117; no remote change was made here) and decides the open items listed
in the report: `FileSecurity` conversions, `RemoveAll` account filters,
lazy path overloads, abandoned `PrivilegeEnabler`, dot patterns of
`Get-ChildItem2 -Filter`, the 17 owner-restore handlers without the
later-command check, unused classes (Decisions 21/22).
2. Gate 3: repeat the affected packaged acceptance (the report's handoff
table: owner restore, `InheritedFrom`, later-command exceptions, filter,
privileges, public objects) before the next candidate is published. No
local upload.
3. Retain stacked-PR order (15), obtain Decision 22 review, finish the OS
matrix and obtain or explicitly accept #34 feedback through other gates.
4. Do not release stable 5.0.0 or equate a percentage with gate closure.
1. The maintainer integrates the rest of the stack (Decision 15; commands in
the deployment notes). A **new** pull request from `ai/quality-gate-coverage`
to `master` replaces #117; then #118 and, if its module fixes go into rc7,
#119 are retargeted with `gh pr edit <n> --base master`, marked ready, and
merged. No `--delete-branch` while another open pull request uses the branch
as its base; the head branches are deleted last. He decides which module
fixes of the matrix branch belong to rc7 (two commits: `962887a` holds two
fixes, `fdd7a8b` one) and reviews them (Decision 24).
2. He decides the open items listed in the paths report: `FileSecurity`
conversions, `RemoveAll` account filters, lazy path overloads, abandoned
`PrivilegeEnabler`, dot patterns of `Get-ChildItem2 -Filter`, the 17
owner-restore handlers without the later-command check, unused classes
(Decisions 21/22).
3. Gate 3: accept the published package in the first lab and in every cell of
the matrix (`Run-MatrixSequence.ps1 -Version`) before the candidate counts as
accepted; no local upload. The paths fixes were accepted locally (record
above).
4. Retain stacked-PR order (15): #116 (merged), the replacement of #117, #118,
then #119; a simulated merge in that order gives exactly the tree of the
matrix branch (`62aa1ae`). Confirm or change Decision 22.
5. #34 stays open (Decision 23): the maintainer chooses between waiting for a
test of the published candidate on the NetApp, EMC, and IBM ESS servers of
the reporters (checklist: `Tests/Lab/Non-Windows-File-Server-Test.md`) and
accepting the untested risk with a release-note caveat. No agent can
accept it.
6. He decides the two LOW findings of the security review (record, Limits):
the swallowed initialization errors of `GetEffectiveAccess` (a false "no
access" instead of an error on an OS that refuses at initialization, and
neither warning nor error when the local fallback fails; fix: record the
initialization exceptions in `authzException`, with a regression test and a
check of the sentence "the error stays" in the help and CHANGELOG), and the
ACL of the stage folders under `C:\` in the lab kit (they inherit
Authenticated Users: Modify; protecting them is a design change of the
controller and needs a new acceptance). The help could also say that a local
standard user who asks about a domain account still gets "Access is denied".
7. Do not release stable 5.0.0 or equate a percentage with gate closure.

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

@ -1,9 +1,9 @@
---
status: proposed
date: 2026-10-08
last-verified: 2026-10-08
last-verified: 2026-10-09
owner: shared
source: agent choices in autopilot on 2026-10-08, for the maintainer's review
source: agent choices in autopilot on 2026-10-08, for the maintainer's review; confirmed by the agent under his delegation on 2026-10-09
---
# Decision 22: The behavior changes of Phase 2
@ -30,7 +30,7 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review
| 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` |
| 10 | `Copy-Item2` doesn't create the missing destination folders (rc6) | Kept, like `Move-Item2`, and for a file like `Copy-Item`. Corrected on 2026-10-09: for a folder, `Copy-Item` creates the missing parent folders and `Copy-Item2` doesn't |
- 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
@ -48,6 +48,60 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review
- 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).
(3); the conventions of .NET and PowerShell (4, 9); no implicit creation
of folders (10; `Copy-Item` creates them for a folder, see the correction
below).
- Open: the maintainer accepts or reverts each choice; then this record
becomes `accepted`.
## Confirmed under delegation, 2026-10-09
- Context: on the evening of 2026-10-09 the maintainer went to bed and told
the agent to continue with the next work and, for any decision that comes
up, to "do it and report about it later". The handoff for this record asks
for one question per item, which nobody could answer overnight. The agent
compared each choice with the changelog and the cmdlet pages, and checked
item 10 against the source and against the built-in `Copy-Item`; it did
not run the tests of the other items again for this record (they ran with
the suite of rc7 and of the later branches). An independent read-only
review checked the statements of the table below against the same pages
and found them accurate except two, which are corrected here (item 10, and
the migration hint of item 8). The agent confirmed all ten choices. This is
the agent's decision under that delegation, not the maintainer's own, so
the status stays `proposed` until he confirms it or reverts an item.
Nothing in the code, the tests, or the help changed.
- Impact for a caller, and where the choice is documented (the changelog
under [Unreleased], and the page of each cmdlet in `Docs\Cmdlets`):
| # | Impact for a caller | Documented |
| --- | --- | --- |
| 1 | Without the Security privilege, `Get-NTFSOrphanedAudit` writes a non-terminating `ReadSecurityError` per item and goes on; an empty result no longer hides unread items. A script that took empty output for "nothing orphaned" now sees errors | `Get-NTFSOrphanedAudit` page, notes |
| 2 | A recursive `Get-NTFSSimpleAccess` reports the folders that earlier versions left out, with their subfolders; the output can have more rows | `Get-NTFSSimpleAccess` page, notes |
| 3 | None: `New-NTFSSymbolicLink` still needs the right to create symbolic links; Developer Mode doesn't help | `New-NTFSSymbolicLink` page, notes |
| 4 | With `-WhatIf`, a conflict at the destination is a verbose message, so `-WhatIf -ErrorAction Stop` no longer stops on it | `Move-Item2` page, description; the changelog |
| 5 | The warning of `Get-NTFSEffectiveAccess` names the computer; a script that matches the old text must change | the changelog |
| 6 | `Move-Item2` writes a `MoveError` for a folder on another volume and leaves the folder in place; before, AlphaFS copied and deleted it, which lost empty folders | `Move-Item2` page, notes; the changelog |
| 7 | **Breaking:** the link cmdlets write a non-terminating error per link and go on; a script that relies on the stop needs `-ErrorAction Stop` | both link pages, notes; the changelog, **Breaking** |
| 8 | **Breaking:** `-Path` and `-Target` are required; a script that omitted one must pass it | both link pages, notes; the changelog, **Breaking** |
| 9 | None: entries and descriptors are equal only as the same .NET object, as in .NET; `Compare-Object -Property` compares values | `Docs\FAQ.md` |
| 10 | Only against the earlier 5.0.0 prereleases, which created the missing parent folders of a folder copy: `Copy-Item2` writes an error that names the missing folder, as `Move-Item2` does. It differs from `Copy-Item`, which creates the missing parents of a folder copy (checked in both editions on 2026-10-09; for a file, `Copy-Item` writes an error as well). A script that relied on the prerelease behavior creates the folder first. Published rc6 and the candidate both write a `CopyError` and create nothing (checked in both editions on 2026-10-09) | `Copy-Item2` page; the changelog |
- Why all ten stand: 5.0.0 is a major version, so documented breaking
changes are allowed (7 has a **Breaking:** entry with a migration hint,
`-ErrorAction Stop`; the **Breaking:** entry of 8 names the old behavior,
and the migration is to pass both parameters); 1, 2, and 6 replace a
result that looked valid with an error or a complete result; 3, 4, and 9
follow the conventions of .NET and PowerShell and add no feature before
the archive; 5 is a clearer message. Item 10 adds no feature either, but
its reference point was wrong: it isn't like `Copy-Item` for a folder.
The stricter behavior is the safer default and matches `Move-Item2`, and
creating missing parents would flip the behavior of rc6 and rc7 again, so
the agent keeps it and leaves the question, whether `Copy-Item2` should
create the missing parents of a folder copy like `Copy-Item`, to the
maintainer. Reverting item 8 would bring back the failure for every object
piped to the link cmdlets (found on the way, above).
- To revert an item: revert its commit (the range in Context), regenerate
the help from `Docs`, adjust the changelog and the cmdlet page, and run
the four test configurations again; a later commit on the same page or
test can conflict.
- Open: the maintainer confirms (`accepted`) or reverts each item.

74
.memory-bank/decisions/0023-non-windows-file-servers.md

@ -0,0 +1,74 @@
---
status: proposed
date: 2026-10-09
last-verified: 2026-10-09
owner: shared
source: agent assessment for the maintainer (Handoff 4), from #34 read on 2026-10-09 21:33 UTC
---
# Decision 23: Non-Windows file servers before 5.0.0 (#34)
- Context: Decision 21 leaves to the maintainer how to cover file servers that
aren't Windows (#34). The issue is open (labels Bug and Help Wanted, 26
comments, last activity 2026-10-06 16:05 UTC). This record separates what
the reporters said from what we tested, and states what the maintainer has
to decide. It accepts no risk: the gate stays open until a tester reports
on the exact candidate or the maintainer accepts the risk in his own words.
- Reporter evidence (text of the issue and its comments, treated as data):
| Date | Who | System and claim |
| --- | --- | --- |
| 2018-08 | deftleft | NetApp Clustered Data ONTAP 9.3P6, Windows 10 1607 and 1709: error 1307 from `Add-NTFSAccess` only on the UNC path of the filer, not on a local drive; the folder is owned by `BUILTIN\Administrators`, the account is a member; `icacls` works |
| 2018-09, 2019-01 | dt1ll0ts0n, FrisbeeGolfer | 1307 on UNC paths; Windows Server 2012 R2 file servers with DFS (a Windows server); builds from 4.0 fail, 3.2.3 works; service account has Full Control and isn't an administrator |
| 2019-10, 2020-01 | Bi00, Marc408 | NetApp behind DFS; it works when the running account owns the folder |
| 2020-01, 2023-11 | jcardel | EMC filer: the owner can be set only through a share that impersonates root; other permissions work over SMB; the owner entry "Owner Rights" is his workaround |
| 2023-05 | tberta | EMC NAS, 4.2.6, no administrator rights on the NAS: 1307; Process Monitor shows the owner written in addition to the DACL, while `icacls` writes only the DACL |
| 2023-11-28 | maintainer | reproduced with a customer: the user isn't a local administrator and lacks the backup and restore privileges |
| 2026-10-05, -06 | maintainer | the fix writes only the changed section (Decision 19); rc3 announced as published on 2026-10-06 13:40 UTC; asked for a tester on NetApp or EMC |
| 2026-10-06 16:05 | jcardel | moving to IBM ESS (UNIX), owner issue "still the same" there; will test both systems and report "tomorrow" |
No reply followed by 2026-10-09 21:33 UTC. Silence is not success. The
2020 comments of Kluk and agonzalezm describe other causes (a script that
wasn't run as administrator; a name that can't be translated).
- What we tested: only Windows. The lab comparison of 2026-10-07 reproduced
the error over SMB with rc2 and showed rc4 passing; every candidate since,
including `83149ee` on 2026-10-09, passes case 1 (a delegated account that
doesn't own the folder: add, remove, clear, disable and enable inheritance,
set inheritance, and security descriptor, with the owner kept) in both
editions on Windows Server 2025. CI reproduces the error without a file
server. The matrix of Decision 21 adds Server 2019, 2022, and Windows 11.
- What it doesn't show: whether NetApp ONTAP, Dell EMC, or IBM ESS accepts a
write of the DACL alone from an account that isn't their administrator, and
what else they refuse. Hypotheses, not facts: they may refuse a flag that
the cmdlets set (for example the protected-DACL flag when the inheritance
changes), or map the ACL differently. `Set-NTFSOwner` can't work where the
server doesn't allow assigning an owner; that is a server policy.
- Options for the maintainer:
1. Wait for a report on the exact candidate (rc7 once published) from both
reporters. Safest; the stable release waits for an unknown time.
2. Accept the untested risk, and release with the caveat below. The
decision is security-relevant, so only the maintainer can take it; it
doesn't go through a "not sure, you pick" answer.
3. Both: publish rc7, ask for tests with the checklist, and choose a date
after which you decide between 1 and 2.
- Recommendation: option 3. Nothing in the module changes for #34 meanwhile.
- Caveat for the release notes, if the risk is accepted (adjust the list of
operating systems to what the matrix has tested): "The fix for #34, which
writes only the section of the security descriptor that a command changes,
was tested on Windows file servers. NetApp, EMC, and IBM ESS file servers
weren't available for 5.0.0. If a command still fails there with error
1307, tell us in #34. `icacls` is the fallback."
- Open: the maintainer's choice between the options, and the date. Until
then the gate of Decision 21 for #34 is open.
- Tester checklist: `Tests/Lab/Non-Windows-File-Server-Test.md`. It uses a
new folder that the tester controls and asks for sanitized evidence.
- Draft comment for #34 (for the maintainer to post; the agent posts
nothing; replace the version and the link when they exist):
```text
Thanks again for offering to test, @jcardel, and thanks @tberta for the Process Monitor capture that showed the owner write. Since 5.0.0-rc3, we have published more prereleases. The one to test is 5.0.0-rc7, because it is the candidate that becomes 5.0.0. Please test it on both systems you mentioned, with an account that is not an administrator or root of the file server.
Use a new folder that you create for the test, never real data, a share root, or a home folder. The steps take about 20 minutes: <link to Tests/Lab/Non-Windows-File-Server-Test.md>. Please report the package version, the file server product and version, and for each command whether it worked, the first line of any error, and the owner before and after. Please don't post passwords, keys, file contents, or complete security descriptors, and replace names with placeholders.
A report of "it works" without these details can't tell us which command ran on which setup. If something fails, that is just as useful: the error and the owner before and after show what the file server refuses. We would like to have your result before 5.0.0. NTFSSecurity will be archived after 5.0.0.
```

150
.memory-bank/decisions/0024-os-matrix-lab.md

@ -0,0 +1,150 @@
---
status: proposed
date: 2026-10-09
last-verified: 2026-10-10
owner: shared
source: agent decisions under the maintainer's delegation of 2026-10-09 (Handoff 2); the scope follows Decision 21, phase 3
---
# Decision 24: The operating-system matrix lab
- Context: Decision 21 requires the live tests on more operating systems,
"such as a Windows 11 client and Server 2019 and 2022 file servers", and the
published package must pass them. Every machine of `WindowsAccessControlLab`
is Server 2025. Handoff 2 asks for the maintainer's approval of scope and
topology before new VMs. On 2026-10-09 at 21:21 UTC the maintainer, going to
bed, wrote "you can do whatever is required with the lab" and told the agent
to decide and report later. The agent took that as the approval for the
minimal matrix below and for nothing broader. It is the agent's decision, so
the status stays `proposed` until the maintainer confirms it.
- Choice:
1. Cells: a Windows 11 client with each file server (Server 2019 Datacenter
10.0.17763.1217, Server 2022 Datacenter 10.0.20348.4773, Server 2025
Datacenter 10.0.26100.32690), the module in Windows PowerShell 5.1 and
PowerShell 7 in every cell. The client was planned as Windows 11 Pro
26H1 (10.0.28000.1836). It cannot keep a secure channel to the Server 2025
domain controller (see "What the deployment showed"), so the domain client
is `OSWin11E`, Windows 11 Enterprise Evaluation 22H2 (10.0.22621.525), and
the 26H1 machine `OSWin11` stays in the lab outside the domain for runs of
the module's own tests. The reference cell of Decision 20 (Server 2025
client and file server in `WindowsAccessControlLab`) stays as it is.
Server 2019 and 2022 as clients are extra cells, to run the module on the
older .NET Framework builds (Server 2019 has 4.7.2).
2. Topology: a separate AutomatedLab lab `NtfsSecurityOsMatrixLab` with its
own internal switch (`192.168.12.0/24`) and its own forest `osmatrix.net`:
`OSDC1` (Server 2025, root domain controller), `OSFile19`, `OSFile22`,
`OSFile25`, `OSWin11E`, and `OSWin11`. `Deploy-OsMatrixLab.ps1` deploys it
with the maintainer's AutomatedLab and the VM path `V:\AutomatedLab-VMs`;
`Add-OsMatrixMachine.ps1` adds a machine to the deployed lab; the
payloads of the existing lab (PowerShell 7.6.3 and Pester 5.7.1 from the
host, because the VMs have no internet) come from
`Complete-OsMatrixLab.ps1`.
3. Case 9 (accounts of other domains and forests) needs trusts to the
forests of the existing lab, so the matrix cells run with
`-ForeignDomainController @()`; the existing lab keeps that case. The final
candidate ran it there (run `fl1`: 245 passed, 0 failed, 1 skipped per
edition, 16 case-9 tests per edition).
4. The controller of the repository runs in every cell with `-LabName`,
`-DomainController`, `-FileServer`, and `-Client`. The matrix showed three
defects of its setup and removal, fixed in `7d47316`: a recursive delete
fails with "The directory is not empty" on Windows Server 2019 (and the
stderr line ended the script before any retry, because `2>&1` under `Stop`
is terminating in Windows PowerShell 5.1), `Get-LocalGroupMember` fails on
an orphaned SID, and a vanished profile failed the client cleanup.
5. The module's own behavior tests run on every machine as well
(`Run-MatrixLocalSuite.ps1`), elevated and as a basic user, in both
editions, as scheduled tasks so that the token matches a CI runner. This
found three defects of the module, fixed in two commits on
`ai/quality-gate-lab-matrix`: `Get-NTFSInheritance -SecurityDescriptor`
and `Get-NTFSEffectiveAccess -ServerName ''` (`962887a`, two fixes), and
`Get-NTFSEffectiveAccess` for a user who isn't an administrator on a
computer in a domain (`fdd7a8b`, with a live test for the ServerAdmin
role). The maintainer decides which of them belong to rc7.
- Why a separate lab: AutomatedLab 5.61 refuses to add machines to an
imported lab, and defining a lab under an existing name would overwrite the
metadata of its 13 machines. A separate lab leaves every shared machine,
switch, domain, and account untouched, which Handoff 2 requires. Inside the
new lab, a machine can be added with `Import-LabDefinition`,
`Add-LabMachineDefinition`, and `Export-LabDefinition`, followed by the steps
that `Install-Lab` runs for one machine; `Add-OsMatrixMachine.ps1` does this
after it copies the lab metadata.
- Cost and rollback: six VMs (4 GB for the domain controller and both clients,
3 GB for each file server), four new base images, measured at 17.8 GB for
the differencing disks and 42.4 GB for the base images (about 60 GB on `V:`;
my first estimate of 100 GB was too high). The deployment added twelve lines
to the hosts file of the host, which `Remove-Lab` removes. To remove the
matrix, run `Remove-Lab -Name NtfsSecurityOsMatrixLab` from AutomatedLab;
nothing else depends on it. No existing machine, checkpoint, or lab was
changed. A copy of the lab metadata from before the sixth machine is in
`C:\ProgramData\AutomatedLab\Backups` (administrators only).
- What the deployment showed (the agent's decisions D11 to D23 of the night
log, each reversible):
- The base image of a Server 2019 or a Windows 11 22H2 machine had an empty
EFI system partition: the `bcdboot` of the Server 2025 host fails with
exit code 193 on their boot files, and AutomatedLab ignores the exit code,
so the generation 2 machine fails with Hyper-V event 18603. The images of
Server 2022 and Windows 11 26H1 are fine. `Repair-OsMatrixBoot.ps1` runs
the `bcdboot` of the image itself on the differencing disk of the one
machine and starts it.
- The AutomatedLab driver sat in its file-server job wait with idle remote
runspaces after all features were installed, so I stopped it and ran the
rest by script.
- Windows 11 26H1 (10.0.28000.1836) joins the domain but cannot keep the
Netlogon secure channel to the Server 2025 domain controller
(10.0.26100.32690). The client calls `NetrLogonGetCapabilities` with query
level 2, which the protocol document describes as a check of the flags the
client sent; the controller answers `STATUS_ACCESS_DENIED` (level 1 and
`NetrServerAuthenticate` succeed), and the client denies the channel
(`NlConfirmRequestedCapabilities: denying access ... 0xc0000022`).
`Test-ComputerSecureChannel -Repair` can't help. Windows 11 22H2 against the
same controller works (secure channel, Kerberos, readiness). This is an
environment finding about two Microsoft builds, not about NTFSSecurity; the
maintainer may want to know it for his own labs.
- The Windows 11 Enterprise Evaluation 22H2 image (`OSWin11E`) is in
notification mode from its first day and shuts down an hour after every
start (`wlms.exe`, 0xC004F009 "grace time expired"): its install time was
recorded on a clock about seven hours ahead, which was then corrected. One
of its two rearms didn't help. After an unplanned shutdown its machine
account password no longer matched (domain logons fail with 0xC000018D,
`nltest /sc_verify` says `ERROR_INVALID_PASSWORD`);
`Test-ComputerSecureChannel -Repair` with the lab account fixed it, and
`/sc_verify` kept showing the old status afterwards, so test a domain
session instead. A run on this machine has to stay under an hour from its
start.
- A profile of an account that a probe's scheduled task used stayed loaded on
one server until it restarted; the probe now uses a new account name for
every run.
- The matrix cells of the live controller failed in the effective-access tests
of the Admin role in the Windows Server 2022 cell (`rc7f`, `rc7h`, `rc7i`,
and `rc7j`) in cells that followed each other, where the fixture was removed
after a cell and created again with the same account names. This looked like
a regression of the module (the audit read of `962887a` was the first suspect)
until a replay of the same cells with the baseline and the final candidate
alternating (`ab0` to `ab6`) failed the baseline in two of three cells and
the final candidate in one of three (not counting the warm-up `ab0`). In a
failing cell the remote
authorization managers (the client's and the file server's) returned no
groups for the current account while the Kerberos S4U logon, the name
resolution, and the local manager were right in the same second. One model,
in which a remote manager answers for an account name for about ten minutes
after its first request, fits all 43 Admin-role runs of 27 cells; the
predictions that I wrote down before three of the replay cells held (the
weakest is `ab6`, 10.5 minutes after its entry, above the lifetimes that
fit). The mechanism in Windows isn't known. The controller now gives a new
fixture a new name for the account of case 3 (`1dec389`); four more cells
with it (`ab7` to `ab10`) passed, two of them at positions where the model
predicts a failure for a reused name. The record has the evidence.
- Result: [the record](../../Tests/Lab/Acceptance-2026-10-10-os-matrix.md). The
final candidate (`fdd7a8b`) passes the module's suite on all five machines
and the host in all four configurations, and the live cells (see the record).
- Open: the maintainer confirms or changes the matrix and decides whether to
keep the VMs after 5.0.0. Local `-ModulePath` runs are validation; the gate
needs the published package in every cell (Handoff 3, stage D). The newest
Windows 11 build that can join a Server 2025 domain here is 22H2; a domain
cell with 26H1 needs a newer domain controller build or a fix of the
mismatch. The maintainer also decides which of the module fixes belong to
rc7 (two commits: `962887a` holds two fixes, `fdd7a8b` one; `fdd7a8b` reverts
cleanly on its own, `962887a` conflicts with it in `Lib.cs` and `CHANGELOG.md`
if `fdd7a8b` stays), and whether the
evaluation client stays (it needs a start shortly before every run) or is
replaced by a client with a license that doesn't expire.

171
.memory-bank/deployment-notes.md

@ -0,0 +1,171 @@
---
status: current
last-verified: 2026-10-10
owner: software-engineer
source: release gates of 5.0.0 (lab acceptance, OS matrix, publication plan), repository evidence
---
# Deployment notes
## Publish the next prerelease (rc7)
State on 2026-10-10 at 09:59 UTC: #116 (rc7, head `d25647d`) is merged into
`master` (merge commit `8a6be9f`, 09:10:12Z). #117 (head `f11ff41`, base
`ai/release-5.0.0-rc7`) was **closed without a merge** at 09:10:16Z: the
`--delete-branch` of `gh pr merge 116` deleted its base branch, and GitHub
closed it (events `base_ref_deleted`, then `closed`) instead of retargeting it.
Nothing is lost: `ai/quality-gate-coverage` is intact at `f11ff41`, and
`master` still lacks its change (25 files). #118 (draft, head `83149ee`, base
`ai/quality-gate-coverage`) and #119 (draft, head `49734ef`, base
`ai/quality-gate-paths`) are open and green. A simulated merge chain
(`git merge-tree --write-tree`, no ref written) with merge commits
(Decision 15) is conflict-free at every step: the coverage branch into `master`
gives the tree of `f11ff41`, #118 then gives `b1dc006`, and #119 gives
`62aa1ae`, the tree of the matrix branch. The manifest says `5.0.0` with
`Prerelease = 'rc7'`, and `$publishedVersions` in `Tests/Repository.Tests.ps1`
lists the versions up to rc6, as it must before rc7 is published.
The branch `ai/quality-gate-lab-matrix` (draft #119) is stacked on #118. It
holds three fixes of the module in two commits (`962887a` has two, `fdd7a8b`
one). `fdd7a8b` reverts cleanly on its own; `962887a` doesn't
revert while `fdd7a8b` stays (the two conflict in `Security2/Win32/Lib.cs` and
`CHANGELOG.md`), and its two fixes go together. The branch also holds the kit of the
operating-system matrix, the changes of the live controller, and the record
(Decision 24). rc7 contains the module fixes only if the branch is merged after
#118 and before the tag; otherwise they go to the next prerelease. The
maintainer decides.
Do not delete a head branch while another open pull request uses it as its
base. On 2026-10-10 the deletion through `gh pr merge --delete-branch` closed
#117 instead of retargeting it. The open reports `cli/cli#1168` and
`cli/cli#14223` show the same two events and say that GitHub retargets only
when the branch is deleted with the button on the pull request page. The
latter also reports, and this was not tried here, that `gh pr edit --base`
refuses a closed pull request and that `gh pr reopen` refuses while the base
branch is missing. A new pull request from the same head is the repair.
1. Open a new pull request from `ai/quality-gate-coverage` to `master` (it
replaces #117, same head and title), wait for its CI, and merge it with
**Create a merge commit**. Do not delete the branch yet.
2. Retarget #118 (`gh pr edit 118 --base master`), mark it ready, and merge it
the same way. Then do the same for #119 if its module fixes go into rc7.
A retarget doesn't start CI again (`pull_request` in `ci.yml` has the
default event types), and the merge result is the tree that CI tested.
3. Delete the head branches only after the last pull request that uses one as
its base is merged or retargeted.
4. Tag the merge commit on `master` with `5.0.0-rc7` and push the tag. The
`release` job checks the tag against the manifest and builds nothing new:
it publishes the package that the `build` job tested. Approve the
deployment of the `powershell-gallery` environment if it asks.
5. After the publication, add `5.0.0-rc7` to `$publishedVersions` with the
next change that goes to `master`.
## Accept a published package
Local `-ModulePath` runs are validation; the gate needs the published bytes.
1. `Tests/Lab/Acceptance/Test-PublishedRelease.ps1 -Version <version>
-OutputPath <folder>` (read-only): tag and commit on `master`, the CI run
of the tag, the Gallery's SHA-512 against the downloaded nupkg (ordinal,
case-sensitive base64), the nupkg against the GitHub zip file by file, and
the identity of the manifest. Dry run on rc6: all checks passed.
2. `Tests/Lab/Invoke-NTFSSecurityLabTest.ps1 -Version <version>` in the
existing lab, both editions, and `Tests/Lab/Acceptance/Run-MatrixSequence.ps1
-Version <version>` for each cell of the matrix (Decision 24; pass the file
servers as one quoted string, `-FileServer 'OSFile19,OSFile22,OSFile25'`, and
start `OSWin11E` shortly before, because its license period ends an hour
after each start). Check every role from the result files with
`Validate-LabResults.ps1`, never from the marker `DONE` of the controller.
Dry run of the `-Version` path of the sequence runner with the published rc6
on OSFile19 (Desktop): the mechanics work, the failing tests are the newer
ones that rc6 predates, and the cleanup verdict was CLEAN. The first lab ran
the final local candidate through the same stages (readiness, controller,
`Validate-LabResults.ps1`, snapshot, `-RemoveFixture`, `Test-MatrixCleanup.ps1`
with the four domain controllers and both machines) in one detached driver.
3. Remove the fixture and check the end state independently with
`Test-MatrixCleanup.ps1`, which takes the lab name and the machine names
(`-LabName WindowsAccessControlLab -DomainController F1ADC1, F1BDC1, F2DC1,
F3DC1 -Machine F1AFile1, F1AFile2` for the existing lab).
4. If the published binary changes, repeat the cells; never combine runs of
different binaries into one matrix.
## Lab lessons
- AutomatedLab 5.61 can't add machines to an imported lab (`Add-LabMachineDefinition`
throws "Lab is already imported"), and `New-LabDefinition` under an existing
name overwrites its metadata. New machines go into a new lab with its own
switch and domain, and `-LabName` of the controller selects it.
- `Install-Lab -NetworkSwitches -BaseImages` creates the switch and the base
images first; the base images of Server 2019, Server 2022, and Windows 11 Pro
took about two to four minutes each from the ISO files. AutomatedLab
adds records to the hosts file of the host, which `Remove-Lab` removes.
- The VMs have no internet: take PowerShell 7 and Pester 5.7.1 from the host
(`Copy-LabFileItem`, `Install-LabSoftwarePackage`).
- AutomatedLab ignores the exit code of `bcdboot` when it builds a base image.
The Server 2019 image that it built on this Server 2025 host got an empty
EFI system partition: the `bcdboot` of the host fails with exit code 193,
"Failure when attempting to copy boot files", on the 2019 boot files, and the
VM failed to boot (Hyper-V event 18603, "failed to boot an operating
system"; no memory demand, no IP, heartbeat `NoContact`). Check the EFI
system partition of a new base image before the first VM: mount the image
read-only (`Mount-DiskImage -Access ReadOnly`) and look for
`EFI\Microsoft\Boot\bootmgfw.efi` (the images of Server 2022 and Windows 11
Pro had 140 and 149 files). Repair a VM, not the base: stop the VM, mount its
own differencing disk, run the `bcdboot.exe` of the image (`D:\Windows\System32\bcdboot.exe
D:\Windows /s H: /f UEFI`), copy `bootmgfw.efi` to `EFI\Boot\bootx64.efi`,
dismount, and start the VM. A changed base image would invalidate its
differencing disks.
- The tool output of the agent masks text that looks like a secret, such as
`-Password $password`, in what it shows. Test such a line by parsing the
file, and don't repair it from the displayed text.
- A script that a detached process runs needs its own log, an exit marker, and
an end-state check of its own; verify cleanup from the end state, not from
its marker.
- Extend a deployed lab with one machine like this: in a process that never ran
`Import-Lab`, call `Import-LabDefinition`, `Add-LabMachineDefinition`, and
`Export-LabDefinition`, then `New-LabBaseImages` and `New-LabVM -Name <machine>`.
`Install-Lab` has no per-machine selector, and `Add-LabMachineDefinition`
throws as soon as `Get-Lab` returns a lab. `Add-OsMatrixMachine.ps1` does it
after it copies the lab metadata to `C:\ProgramData\AutomatedLab\Backups`.
- Windows 11 22H2 (10.0.22621) has the empty EFI system partition problem too
(`bcdboot` exit code 193 on the host); `Repair-OsMatrixBoot.ps1` repairs it.
After `Mount-VHD` the host gives the NTFS partition a letter on its own; don't
assign a second one.
- Run AutomatedLab processes one after the other. Two `Import-Lab` calls at the
same time corrupt each other (XML errors, "No machines imported").
- Check the secure channel of every domain client before the first run
(`Test-MatrixReadiness.ps1`). Windows 11 26H1 (10.0.28000.1836) joins a
Server 2025 domain (10.0.26100.32690) but loses the channel: the client asks
`NetrLogonGetCapabilities` for query level 2, the domain controller answers
`0xC0000022`, and the client denies the channel. Rejoining doesn't help.
- A process that starts from a remoting session has every privilege enabled
and no credentials of its own, so tests that expect disabled privileges fail
(eight per edition). Run the suite of a VM as a scheduled task with a batch
logon at the highest run level (`Register-ScheduledTask -RunLevel Highest
-User -Password`): that token matches a CI runner. A restricted token (a basic
user) can't run Pester's NUnit export, because it asks WMI for the
environment, so write the JSON summary first.
- In Windows PowerShell 5.1, `$PSScriptRoot` is empty in a parameter default of a
script that runs with `-File`; compute it in the body. `-File` passes an array
as one string, so split on commas. With `$ErrorActionPreference = 'Stop'`, a
line that a native command writes to stderr and that `2>&1` redirects is a
terminating error; let the command write its errors to stdout.
- `Get-LocalGroupMember` fails with "Failed to compare two elements in the array"
when the group holds an orphaned SID. Add members with `Add-LocalGroupMember`
and ignore `MemberExistsException`; read and remove members with
`net localgroup <name>` and `net localgroup <name> <SID> /delete`. The SIDs
of a deleted account can't be found afterwards, so keep `fixture-sids.json`
from before the removal of the organizational unit.
- An account that is deleted and created again with the same name made the
remote authorization managers (the client's and the file server's) answer for
about ten minutes as if it had no groups, so `Get-NTFSEffectiveAccess` returned
no access; when the accounts are created again within seconds, the Kerberos S4U
logons returned the old account on the domain controller and member servers for
7 to 15 minutes. The five remedies tried (a ticket purge, `nltest /sc_reset`, a
DNS flush, a restart of the Kerberos service, and waiting) helped only by
waiting (see `techContext.md`). The controller names the account of case 3 anew
for each new fixture; a script of your own that recreates accounts needs unique
names too.
- Restart the evaluation client (`OSWin11E`) right before a sequence or a suite,
not before several: it shuts down an hour after each start. The restart takes
about two and a half minutes and may need the repair of the secure channel.

66
.memory-bank/progress.md

@ -1,6 +1,6 @@
---
status: current
last-verified: 2026-10-09
last-verified: 2026-10-10
owner: active-agent
source: repository and validation evidence
---
@ -10,11 +10,16 @@ source: repository and validation evidence
## Current status
5.0.0-rc6 is published on the Gallery and GitHub; its failed Release job
recovered in attempt 2 on 2026-10-09. #116 (rc7, `d25647d`, base `master`)
is open and green, not merged or published. Further quality-gate work is
local: `ai/quality-gate-coverage` (#117) and `ai/quality-gate-paths`, which
classifies every remaining unvisited path; the open items are the
maintainer's decisions. Stable Gallery version: 4.2.6.
recovered in attempt 2 on 2026-10-09. #116 (rc7, `d25647d`) was merged into
`master` on 2026-10-10 (`8a6be9f`); rc7 is neither tagged nor published. #117
was closed unmerged when its base branch was deleted, so a new pull request
from `ai/quality-gate-coverage` replaces it. Further quality-gate work is
`ai/quality-gate-paths` (draft #118), which classifies every remaining
unvisited path (the open items are the maintainer's decisions), and
`ai/quality-gate-lab-matrix` (draft #119, stacked on #118): the
operating-system matrix, three fixes of the module found by it, and the
controller changes (Decision 24, proposed).
Stable Gallery version: 4.2.6.
After 5.0.0, archive in favor of WindowsAccessControl (Decision 18).
## Recent milestones
@ -74,6 +79,46 @@ After 5.0.0, archive in favor of WindowsAccessControl (Decision 18).
through the error stream) and a privilege left enabled. Nine static
passes of the built-in code-review agent: no Blocker or Major. Report in
`Tests/Coverage`.
- 2026-10-09: lab acceptance of those fixes, `83149ee` against its base
`f11ff41` with the same 244 tests per edition (78 new, case 10): candidate
486 passed, 0 failed, 2 expected skips; baseline 338 passed, 148 failed, all
148 green on the candidate; fixture removed and verified clean on six
machines. Record: `Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md`.
- 2026-10-09 to 10: handoffs 2 to 4 under the maintainer's delegation (decisions
D1 to D46 in the night log of the session files). The matrix lab
`NtfsSecurityOsMatrixLab` (Server 2019, 2022, and 2025 file servers, Windows 11
Enterprise 22H2 client, Windows 11 26H1 suite only) found three defects of the
module, fixed in `962887a` and `fdd7a8b`: audit inheritance by descriptor,
`Get-NTFSEffectiveAccess -ServerName ''`, and the same cmdlet for a user who
isn't an administrator on a domain member. The final candidate passes the
module's suite on every machine (24 runs, no failure) and the live controller
in three cells (1,374 passed, 0 failed, 12 skipped) and in the first lab with
case 9 (245 passed, 0 failed, 1 skipped per edition). The failures of the
effective-access tests in the Server 2022 cell were not a defect of the module:
in a replay of the same cells the baseline failed two of three and the final
candidate one of three (not counting the warm-up cell), and one model (the
remote authorization managers answer for an account name for about ten minutes
after the account was created again) fits all 43 Admin-role runs of 27 cells;
the Windows mechanism is unknown. The controller
names the account of case 3 anew for each fixture (`1dec389`). A read-only
built-in review of the kit and the fixes approved with Minor findings, fixed in
`db04ef2`. A second review of the later commits found one Major (the record
called the cause settled without a baseline replay), addressed by the replay,
`9344ff7`, and `ab0d8e1`; a follow-up review of those fixes found no Blocker or
Major and five Minors, corrected in `e2384e5` and `70f494a`; a second
follow-up review (the first-lab run and the cleanup changes) found no Blocker or
Major and four Minors, corrected in `b78784d` and `dbb4bd6`; the built-in
`security-review` agent found no exploitable vulnerability and two LOW items
that are not changed (record, Limits). Record:
`Tests/Lab/Acceptance-2026-10-10-os-matrix.md`; the agent pushed nothing.
- 2026-10-10: the maintainer merged #116 (`8a6be9f`, 09:10:12Z) with `gh pr
merge --delete-branch` and pushed the matrix branch as draft #119 (`49734ef`).
The deletion removed the base branch of #117, and GitHub closed #117
unmerged three seconds later instead of retargeting it (events
`base_ref_deleted`, `closed`; the same pair is in `cli/cli#14223`). The
earlier guidance, which relied on a retarget, was wrong. Nothing is lost; a
new pull request from `ai/quality-gate-coverage` replaces #117 (deployment
notes).
## Stable capabilities
@ -123,9 +168,12 @@ After 5.0.0, archive in favor of WindowsAccessControl (Decision 18).
tests and exact artifact SHA-512 verification. Original upload errors
remain errors for missing/different/unverifiable outcomes. Not deployed
until the maintainer merges/pushes; no publication was performed here.
9. Phase 3: choose OS scope (proposed Windows 11 client/2019/2022 servers),
detect ISO editions, provision without repurposing shared VMs, then run
published-package acceptance. #34 has no new reply since 2026-10-06.
9. Operating-system matrix (Decision 24, proposed): the lab and the cells exist
and the final local candidate passes them. Open: the acceptance of the
published rc7 in every cell, keeping or replacing the VMs (about 60 GB) and
the evaluation client (it shuts down every hour), which module fixes go to
rc7, and a domain cell for Windows 11 26H1. #34 has no new reply since
2026-10-06.
10. Lab rollback evidence: new checkpoints exist but report Standard even
after a successful temporary ProductionOnly probe. Classification is
unresolved; original VM policy restored, no checkpoint restored. Do

19
.memory-bank/systemPatterns.md

@ -47,6 +47,8 @@ Read only task-relevant records; the index controls routing.
| 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) |
| 23 | [Non-Windows file servers before 5.0.0, #34 (proposed)](decisions/0023-non-windows-file-servers.md) |
| 24 | [The operating-system matrix lab (proposed)](decisions/0024-os-matrix-lab.md) |
## Patterns
@ -112,6 +114,23 @@ Read only task-relevant records; the index controls routing.
example code blocks, and check the generated XML.
- Live tests use only approved lab targets, SMB then independent server state;
Get/SetFileSecurity preserves stored DACLs; rights oracles use S4U tokens.
- A suite that is green on the development host and on CI says little about a
feature that the environment lacks. The matrix found three defects that every
earlier run had missed because the host is outside a domain and the CI
runner's token differs: run the suite on a domain member, on other builds, and
as a basic user before a release, and classify a failure by a probe under the
real tokens (elevated, filtered, local standard, domain standard) before
calling it a defect or a design.
- A fixture that deletes an account and creates it again with the same name can
get a wrong answer for about ten minutes: the remote authorization managers
answered `0x100000` for the current SID while the local manager and a Kerberos
logon were right in the same second, and, when the accounts are created again
within seconds, the Kerberos S4U logons returned the old account (7 to 15
minutes). A failure that follows the order of the cells and not the version of
the module points to such state: run the baseline and the candidate in cells
that follow each other and alternate them (the replay of the record) before
blaming the code. The controller names the account of case 3 anew for each new
fixture.
### CI results and publication

69
.memory-bank/techContext.md

@ -175,5 +175,74 @@ source: repository and executable evidence
- Remote Authz answers administrators and Access Control Assistance
Operators (S-1-5-32-579); other accounts get access denied. Check firewall
when remote resource-manager RPC fails. Expected rights use S4U tokens.
A computer in a domain offers the remote interface to every caller, so the
denial also hit the default `-ServerName localhost` for a user who isn't an
administrator; a computer outside a domain doesn't offer it, which is why the
tests passed on the development host and on CI. Since `fdd7a8b`, the local
manager answers for a name of this computer when the remote one refuses; the
denial stays for another computer (live test of the Delegate role).
- A live test is evidence of a fix only when it fails on the build without
the fix: run the same tests, controller, and lab against the candidate and
the base of the branch, a new process per edition, and join both result
sets by edition, role, and full test name; the tests that pass on both are
controls (`Tests\Lab\Acceptance-2026-10-09-quality-gate-paths.md`). A
validator must not name a loop variable like a typed parameter: PowerShell
variables ignore case, so `$edition` overwrote `$Edition` and every edition
in the CSV became `System.String[]`.
- RemoveFixture after the run; verify OUs/accounts, share, folders, local
memberships, and test profiles removed. Credentials must never be printed.
### Operating-system matrix (Decision 24)
- Lab `NtfsSecurityOsMatrixLab`: OSDC1 (Server 2025), OSFile19/22/25 (Server
2019/2022/2025), OSWin11E (Windows 11 Enterprise Evaluation 22H2, the domain
client), OSWin11 (Windows 11 Pro 26H1, suite only). Kit: `Tests\Lab\Acceptance`;
record: `Tests\Lab\Acceptance-2026-10-10-os-matrix.md`.
- Run the module's own suite on every machine class before the controller
(`Run-MatrixLocalSuite.ps1`, elevated and basic, both editions, as scheduled
tasks with a batch logon at the highest run level): a child of a remoting
session has every privilege enabled and fails eight tests that expect them
disabled. Skipped lists are compared as multisets against the host.
- AutomatedLab: one `Import-Lab` at a time, and none while a controller
sequence runs (it re-imports the lab); `Wait-LabVM` waits for a heartbeat that
a client may not report, so retry `New-LabPSSession`. The host's `bcdboot`
leaves the ESP of a Server 2019 or Windows 11 22H2 base image empty.
- Windows PowerShell 5.1: `$PSScriptRoot` is empty in a parameter default under
`-File`; `2>&1` on a native command under `Stop` makes its stderr line
terminating; `Get-LocalGroupMember` fails on an orphaned SID; `net localgroup
<name> <SID> /delete` refuses the SID of a name that its cache still
resolves (use `Remove-LocalGroupMember -SID`).
- Windows 11 26H1 (28000.1836) loses the secure channel to a Server 2025 domain
controller (`NetrLogonGetCapabilities` level 2, 0xC0000022): suite only.
The 22H2 evaluation client shuts down every hour (license grace expired) and
can lose its machine password after an unplanned shutdown: keep a run under
an hour from its start, test a domain session (not `nltest /sc_verify`, which
stays stale), repair with `Test-ComputerSecureChannel -Repair`.
- Builds are not byte-reproducible (two unchanged assemblies differ per build):
hash each candidate and its package separately.
- The fixture's account for case 3 gets a new name for each new fixture
(`NtfsLiveSubject` and four digits). In the matrix lab, after an account was
deleted and created again with the same name, the remote authorization
managers (the client's for the default `-ServerName`, the file server's for its
name) answered for about ten minutes as if it had no groups (`0x100000`), for
the baseline and for the final candidate alike, while the Kerberos S4U logon of
the oracle, the
name resolution, and the local manager were right in the same second. A replay
with the baseline and the final candidate alternating failed the baseline in two
of three cells and the final candidate in one of three (not counting the warm-up
cell). The mechanism in Windows is unknown; a model with one lifetime (9.95 to
10.25 minutes for both tests, to within 0.05 minute) fits all 43 Admin-role runs
of 27 cells. When the accounts are created again within seconds, the S4U
logon itself returns the old account for 7 to 15 minutes. A `klist purge`,
`nltest /sc_reset`, a DNS flush, and a restart of the Kerberos service didn't
help. `Probe-AccountRecreation.ps1`, `Export-CellTimeline.ps1`, and
`Test-StaleAuthzModel.ps1` show it.
- `net.exe localgroup` lists a local user by its bare name and the entry of a
deleted domain account as its SID (or as its cached name for a while);
deleting a local user removes its entries from the local groups, so only the
entries of domain accounts stay orphaned. `Test-MatrixCleanup.ps1` finds the
entries of the account probe in Performance Log Users by a name with
`NtfsProbe` or by any unresolved `S-1-5-21-…` SID (every such member counts as
the probe's), and its profiles by their folder `C:\Users\NtfsProbe*`. The
cached-name form met real residue in a test; the bare-SID form and a profile
that stays loaded didn't.

22
CHANGELOG.md

@ -185,6 +185,28 @@ The format is based on
- Fix `Get-NTFSInheritance -SecurityDescriptor`, which reported
`AuditInheritanceEnabled` as `$true` for a security descriptor that was
read without its audit section; it now reports `$null`, like `-Path`
- Fix `Get-NTFSInheritance -SecurityDescriptor` for an item without audit
entries on computers where Windows reports its audit entries as protected
from inheritance when it reads all sections of the security descriptor at
once, as it did on domain-joined Windows Server 2022 and 2025 and on
Windows 11: the cmdlet reported `AuditInheritanceEnabled` as `$false`,
while `-Path` reported `$true`. The descriptor now takes the state of the
audit entries from a read of that section alone, like `-Path`, and the
cmdlets that start from such a descriptor no longer see the audit entries
as protected
- Fix `Get-NTFSEffectiveAccess -ServerName ''`, which wrote an "Access is
denied" error instead of the warning for a computer that can't be reached,
on computers where Windows takes an empty name for this one. An empty name
never asks the remote interface of the authorization manager now: the
cmdlet warns and returns the result of this computer on every computer
- Fix `Get-NTFSEffectiveAccess` for a user who isn't an administrator on a
computer in a domain. For a name of this computer, such as the default
`localhost`, the cmdlet wrote the error "Access is denied" and no result for
every account, because the remote authorization manager of a computer
answers only its administrators and the members of Access Control Assistance
Operators. It now uses the local authorization manager of this computer when
the remote one refuses the user, as it already did when the remote one can't
be reached. For the name of another computer, the error stays
- Fix `Get-NTFSOwner`, which wrote a "The pipeline has been stopped" error
for every path when a command such as `Select-Object -First 1` stopped the
pipeline, and which repeated a failed read instead of reporting the

6
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.
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 `-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 remote authorization manager of a computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators. For a name of this computer, the cmdlet uses the local authorization manager when the remote one refuses the user who runs the cmdlet, so a user who isn't an administrator gets the result on a computer in a domain, too; for the name of another computer, 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.
@ -139,7 +139,7 @@ Accept wildcard characters: False
### -ServerName
Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.
Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.
```yaml
Type: String
@ -186,6 +186,8 @@ Before 5.0.0, `-ExcludeNoneAccessEntries` had no effect, and the cmdlet returned
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.
Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is denied" and no result on a computer in a domain, also for a name of this computer, such as the default `localhost`.
## RELATED LINKS
[Get-NTFSAccess](Get-NTFSAccess.md)

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

@ -4957,7 +4957,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<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>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 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 `-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 remote authorization manager of a computer answers only the administrators of that computer and the members of its local group Access Control Assistance Operators. For a name of this computer, the cmdlet uses the local authorization manager when the remote one refuses the user who runs the cmdlet, so a user who isn't an administrator gets the result on a computer in a domain, too; for the name of another computer, 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:description>
<command:syntax>
@ -5001,7 +5001,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5052,7 +5052,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5116,7 +5116,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ServerName</maml:name>
<maml:description>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators; for any other account, the cmdlet writes a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and returns no result for the item.</maml:para>
<maml:para>Specifies the computer whose authorization manager resolves the group memberships of the account. The default is `localhost`. Name the computer that stores the item when you query a network path, because the group memberships known there determine the result; if that computer cannot be reached, the cmdlet falls back to the local authorization manager and warns that the result may be inaccurate. The remote authorization manager of a computer answers only its administrators and the members of its local group Access Control Assistance Operators. For the name of another computer, any other user gets a non-terminating `GetEffectiveAccessError` with the message "Access is denied" and no result for the item; for a name of this computer, the cmdlet uses the local authorization manager instead.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
@ -5168,6 +5168,7 @@ PS C:\&gt; Get-NTFSAudit -SecurityDescriptor $sd</dev:code>
<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-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:para>Before 5.0.0-rc7, a user who wasn't an administrator got the error "Access is denied" and no result on a computer in a domain, also for a name of this computer, such as the default `localhost`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>

8
Security2/FileSystem/FileSystemSecurity2.cs

@ -66,10 +66,18 @@ namespace Security2
// Read together with the SACL, the inherited entries of a DACL without the auto-inherit flag lose their
// inherited flag when the parent folder has no SACL, and writing such a DACL back stores them as explicit
// entries. Read alone, the DACL keeps the flags.
//
// The same goes for the SACL: read together with the other sections, the SACL of an item without audit
// entries is reported as protected from inheritance on some computers (seen on domain-joined Windows Server
// 2022 and 2025 and on Windows 11), while the read of the SACL alone, which Get-NTFSInheritance uses for a
// path, reports it as not protected. The state of the item has to be the same by path and by descriptor.
if (HasAuditSection)
{
var accessSecurity = GetSecurity(item, AccessControlSections.Access);
sd.SetSecurityDescriptorBinaryForm(accessSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Access);
var auditSecurity = GetSecurity(item, AccessControlSections.Audit);
sd.SetSecurityDescriptorBinaryForm(auditSecurity.GetSecurityDescriptorBinaryForm(), AccessControlSections.Audit);
}
RememberSections();

51
Security2/Win32/Lib.cs

@ -18,6 +18,10 @@ namespace Security2
IntPtr pGrantedAccess = IntPtr.Zero;
IntPtr pErrorSecObj = IntPtr.Zero;
// Whether the remote resource manager is the one of this computer. Its remote interface answers only the
// administrators of the computer and the members of Access Control Assistance Operators.
bool remoteResourceManagerIsLocal;
#region GetInheritedFrom
// Returns the source of each entry of the DACL, or of the SACL for audit entries, in the order of the ACL. Before
// 5.0.0-rc6, a descriptor with a SACL returned the sources of the audit entries also for the access entries.
@ -177,6 +181,12 @@ namespace Security2
{
remoteServerAvailable = false;
// An empty name names no computer. Windows takes it for this computer on some computers, where the remote
// interface then refuses the check with "Access is denied", and for an unreachable one on others. So the
// remote interface isn't asked, and the local authorization manager calculates the result, like for any
// name that can't be reached.
if (!string.IsNullOrWhiteSpace(serverName))
{
var rpcInitInfo = new AUTHZ_RPC_INIT_INFO_CLIENT();
rpcInitInfo.version = AuthzRpcClientVersion.V1;
@ -185,13 +195,22 @@ namespace Security2
rpcInitInfo.server = serverName;
SafeHGlobalHandle pRpcInitInfo = SafeHGlobalHandle.AllocHGlobalStruct(rpcInitInfo);
if (!AuthzInitializeRemoteResourceManager(pRpcInitInfo.ToIntPtr(), out authzRM))
if (AuthzInitializeRemoteResourceManager(pRpcInitInfo.ToIntPtr(), out authzRM))
{
remoteServerAvailable = true;
remoteResourceManagerIsLocal = IsLocalComputer(serverName);
return;
}
int error = Marshal.GetLastWin32Error();
bool isLocalComputer = IsLocalComputer(serverName);
// The computer can't be resolved or reached (RPC server unavailable), or it doesn't offer the remote
// interface (endpoint not registered); the local authorization manager calculates the result instead.
if (error != Win32Error.EPT_S_NOT_REGISTERED && error != Win32Error.RPC_S_SERVER_UNAVAILABLE)
// This computer can also refuse the caller, who isn't one of its administrators; its own manager
// answers then, too.
if (error != Win32Error.EPT_S_NOT_REGISTERED && error != Win32Error.RPC_S_SERVER_UNAVAILABLE &&
!(isLocalComputer && error == Win32Error.ERROR_ACCESS_DENIED))
{
throw new Win32Exception(error);
}
@ -199,11 +218,17 @@ namespace Security2
// 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))
if (isLocalComputer)
{
remoteServerAvailable = true;
}
}
GetEffectivePermissions_AuthzInitializeLocalResourceManager();
}
private void GetEffectivePermissions_AuthzInitializeLocalResourceManager()
{
//
// As a fallback we do AuthzInitializeResourceManager. But the results can be inaccurate.
//
@ -218,11 +243,6 @@ namespace Security2
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
else
{
remoteServerAvailable = true;
}
}
private void GetEffectivePermissions_AuthzInitializeContextFromSid(IdentityReference2 id)
{
@ -242,6 +262,21 @@ namespace Security2
{
Win32Exception win32Expn = new Win32Exception(Marshal.GetLastWin32Error());
// A computer in a domain offers the remote interface of its authorization manager to every caller, but
// answers only its administrators and the members of Access Control Assistance Operators; any other
// account gets "Access is denied", whichever account the check is for. For a name of this computer, the
// local authorization manager is the manager of that computer and answers every caller. For another
// computer, the denial stays an error: no access instead would be a wrong result.
if (win32Expn.NativeErrorCode == Win32Error.ERROR_ACCESS_DENIED && remoteResourceManagerIsLocal)
{
remoteResourceManagerIsLocal = false;
userClientCtxt = IntPtr.Zero;
authzRM.Dispose();
GetEffectivePermissions_AuthzInitializeLocalResourceManager();
GetEffectivePermissions_AuthzInitializeContextFromSid(id);
return;
}
if (win32Expn.NativeErrorCode != Win32Error.RPC_S_SERVER_UNAVAILABLE)
{
throw win32Expn;

31
Tests/Inheritance.Tests.ps1

@ -59,6 +59,37 @@ Describe 'Get-NTFSInheritance' {
$bySecurityDescriptor.AuditInheritanceEnabled | Should -Be $byPath.AuditInheritanceEnabled
}
# Windows reports the SACL of an item without audit entries as protected from inheritance on some computers when it
# reads all sections together, and as not protected when it reads the SACL alone (domain-joined Windows Server 2022
# and 2025, Windows 11). The state by descriptor has to follow the state of the item.
It 'Should report the same state as for the path of a <Type> without audit entries' -ForEach @(
@{ Type = 'file' }
@{ Type = 'folder' }
) {
$item = New-TestSandboxItem -Sandbox $sandbox -Name 'Descriptor' -Directory:($Type -eq 'folder')
$byPath = Get-NTFSInheritance -Path $item
$bySecurityDescriptor = Get-NTFSInheritance -SecurityDescriptor (Get-NTFSSecurityDescriptor -Path $item)
$bySecurityDescriptor.AccessInheritanceEnabled | Should -Be $byPath.AccessInheritanceEnabled
$bySecurityDescriptor.AuditInheritanceEnabled | Should -Be $byPath.AuditInheritanceEnabled
}
It 'Should report the disabled audit inheritance of a <Type> as for its path' -Skip:(-not $canChangeAudit) -ForEach @(
@{ Type = 'file' }
@{ Type = 'folder' }
) {
$item = New-TestSandboxItem -Sandbox $sandbox -Name 'Descriptor' -Directory:($Type -eq 'folder')
Disable-NTFSAuditInheritance -Path $item -ErrorAction Stop
$byPath = Get-NTFSInheritance -Path $item
$bySecurityDescriptor = Get-NTFSInheritance -SecurityDescriptor (Get-NTFSSecurityDescriptor -Path $item)
$byPath.AuditInheritanceEnabled | Should -BeFalse
$bySecurityDescriptor.AuditInheritanceEnabled | Should -BeFalse
$bySecurityDescriptor.AccessInheritanceEnabled | Should -Be $byPath.AccessInheritanceEnabled
}
It 'Should report the audit inheritance as $null for a security descriptor without the audit entries' {
$sd = New-Object -TypeName 'Security2.FileSystemSecurity2' -ArgumentList (
(Get-Item2 -Path $file), [System.Security.AccessControl.AccessControlSections]::Access

157
Tests/Lab/Acceptance-2026-10-09-quality-gate-paths-Results.csv

@ -0,0 +1,157 @@
"Edition","Role","Test","Baseline","Candidate","BaselineFailure"
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Admin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Admin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling","Failed","Passed","Expected $null or empty, but got 'TakeOwnership'."
"Core","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable","Passed","Passed",
"Core","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should leave the loop for a break of a later command that takes the error of a nested folder","Failed","Passed","Expected $false, but got $true."
"Core","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should pass on what a later command throws when it takes the error of a nested folder","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should start with folders that the account cannot list","Passed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Clear.txt]'."
"Core","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Descriptor.txt]'."
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Delegate","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Should start with a folder whose permissions the account cannot read, and an item that it can","Passed","Passed",
"Core","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling","Failed","Passed","Expected $null or empty, but got 'TakeOwnership'."
"Core","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable","Passed","Passed",
"Core","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of Admin for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Core","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of Delegate for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Core","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of ServerAdmin for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","ServerAdmin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","ServerAdmin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling","Failed","Passed","Expected $null or empty, but got 'TakeOwnership'."
"Desktop","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable","Passed","Passed",
"Desktop","Admin","Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should leave the loop for a break of a later command that takes the error of a nested folder","Failed","Passed","Expected $false, but got $true."
"Desktop","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should pass on what a later command throws when it takes the error of a nested folder","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should start with folders that the account cannot list","Passed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Clear.txt]'."
"Desktop","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]'."
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Delegate","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Should start with a folder whose permissions the account cannot read, and an item that it can","Passed","Passed",
"Desktop","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling","Failed","Passed","Expected $null or empty, but got 'TakeOwnership'."
"Desktop","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable","Passed","Passed",
"Desktop","Delegate","Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of Admin for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Desktop","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of Delegate for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Desktop","Server","Security descriptors on the file server after the runs on the client.Should have changed only the first item of ServerAdmin for each cmdlet that a later command stopped","Failed","Passed","Expected $true, because Remove-Item2 left the second item (Select), but got $false."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed","Expected 0, but got 1."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected 1, but got 2."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","ServerAdmin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","ServerAdmin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
1 Edition Role Test Baseline Candidate BaselineFailure
2 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
3 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
4 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
5 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
6 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
7 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
8 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
9 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
10 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
11 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
12 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
13 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
14 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
15 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
16 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
17 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
18 Core Admin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
19 Core Admin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
20 Core Admin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
21 Core Admin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
22 Core Admin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
23 Core Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling Failed Passed Expected $null or empty, but got 'TakeOwnership'.
24 Core Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable Passed Passed
25 Core Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
26 Core Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should leave the loop for a break of a later command that takes the error of a nested folder Failed Passed Expected $false, but got $true.
27 Core Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should pass on what a later command throws when it takes the error of a nested folder Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
28 Core Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should start with folders that the account cannot list Passed Passed
29 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
30 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
31 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
32 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
33 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
34 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
35 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
36 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
37 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
38 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
39 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
40 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
41 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
42 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
43 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
44 Core Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
45 Core Delegate An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Clear.txt]'.
46 Core Delegate An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Descriptor.txt]'.
47 Core Delegate Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
48 Core Delegate Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
49 Core Delegate Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
50 Core Delegate InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
51 Core Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry Failed Passed Expected strings to be the same, but they were different.
52 Core Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Should start with a folder whose permissions the account cannot read, and an item that it can Passed Passed
53 Core Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling Failed Passed Expected $null or empty, but got 'TakeOwnership'.
54 Core Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable Passed Passed
55 Core Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
56 Core Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of Admin for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
57 Core Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of Delegate for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
58 Core Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of ServerAdmin for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
59 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
60 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
61 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
62 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
63 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
64 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
65 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
66 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
67 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
68 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
69 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
70 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
71 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
72 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
73 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
74 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
75 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
76 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
77 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
78 Core ServerAdmin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
79 Core ServerAdmin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
80 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
81 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
82 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
83 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
84 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
85 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
86 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
87 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
88 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
89 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
90 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
91 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
92 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
93 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
94 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
95 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
96 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
97 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
98 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
99 Desktop Admin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
100 Desktop Admin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
101 Desktop Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling Failed Passed Expected $null or empty, but got 'TakeOwnership'.
102 Desktop Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable Passed Passed
103 Desktop Admin Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
104 Desktop Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should leave the loop for a break of a later command that takes the error of a nested folder Failed Passed Expected $false, but got $true.
105 Desktop Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should pass on what a later command throws when it takes the error of a nested folder Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
106 Desktop Delegate A later command and the error of a folder that Get-ChildItem2 cannot read on a share.Should start with folders that the account cannot list Passed Passed
107 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
108 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
109 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
110 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
111 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
112 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
113 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
114 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
115 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
116 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
117 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
118 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
119 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
120 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
121 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
122 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
123 Desktop Delegate An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Clear.txt]'.
124 Desktop Delegate An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]'.
125 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
126 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
127 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
128 Desktop Delegate InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
129 Desktop Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry Failed Passed Expected strings to be the same, but they were different.
130 Desktop Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Should start with a folder whose permissions the account cannot read, and an item that it can Passed Passed
131 Desktop Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling Failed Passed Expected $null or empty, but got 'TakeOwnership'.
132 Desktop Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should hold the four privileges that the cmdlets enable Passed Passed
133 Desktop Delegate Privileges when a later command takes the debug messages of the cmdlet on a share.Should pass on what a later command throws at the message after the enabling and disable the privileges Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
134 Desktop Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of Admin for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
135 Desktop Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of Delegate for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
136 Desktop Server Security descriptors on the file server after the runs on the client.Should have changed only the first item of ServerAdmin for each cmdlet that a later command stopped Failed Passed Expected $true, because Remove-Item2 left the second item (Select), but got $false.
137 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
138 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Copy-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
139 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
140 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Move-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
141 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
142 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Remove-Item2 should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
143 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
144 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSOwner should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
145 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for Select-Object -First 1 and change nothing else Failed Passed
146 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.Set-NTFSSecurityDescriptor should stop after the first object for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
147 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed Expected 0, but got 1.
148 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Get-FileHash2 should stop at the verbose message for throw and change nothing else Failed Passed Expected 1, but got 2.
149 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for Select-Object -First 1 and change nothing else Failed Passed
150 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSOwner should stop at the debug message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
151 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
152 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
153 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
154 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
155 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
156 Desktop ServerAdmin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
157 Desktop ServerAdmin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.

251
Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md

@ -0,0 +1,251 @@
# Quality-gate paths follow-up acceptance, 2026-10-09
Live acceptance, in the lab, of the behavior that the fixes of
`ai/quality-gate-paths` change, as the handoff table of the
[path report](../Coverage/Quality-Gate-Paths-2026-10-09.md) asks. The branch
(28 commits on `f11ff41`, the head of #117; head `83149ee`, draft #118) is a
local candidate, tested from its extracted package with `-ModulePath`. It is
not a published package, and this record is not a claim that the quality gate
is complete. Architecture and cmdlet-design choices remain with the
maintainer (Decisions 16, 21, and 22).
## Method
New live tests, case 10 and one test of the Server role, check what each fix
changed. The same tests, controller, and lab ran against two builds, in new
processes for each edition: the candidate (`83149ee`) and the baseline
(`f11ff41`, the base of the branch). A test is evidence of a fix when it
passes on the candidate and fails on the baseline; a test that passes on both
is a control.
## Candidate and artifact identity
| | Candidate | Baseline |
| --- | --- | --- |
| Commit | `83149eedee0684bd0a0522865abd0bc6127f6bf5` | `f11ff412947b35d682878ac4a8121c949868fcb2` |
| `NTFSSecurity.dll` SHA-256 | `40D0C8A6B819F15AE69A21D4D510B3B3CFCE2D93294368046C707BD558E67C1F` | `96F087E2AA39D521018346CC9F0A23C8AE2EE2D8CB39AE0E9B7A9325CF47AB73` |
| `NTFSSecurity.5.0.0-rc7.nupkg` SHA-256 | `2AAE3403A2D1C3AEE5156F05441E46B85B855AF95B46A7B73D2F80435513D71D` | `06244B161F76F3A9DCCCFDE3D7D6C5D0D5FEB625127FBF1B298D935BCBD8A2E2` |
| `NTFSSecurity.zip` SHA-256 | `A5AFA241DCA5DF87080A9801BB336282BD424D70DA395F6456F2D74B7FC8076A` | `3DF287C9AC4A311E4093DE519DED046B94109F51B513ACBC1653EF483DB3A2C0` |
- Each build is a Release build (.NET Framework 4.5.2) in an isolated worktree
of its commit, packaged by `.github/scripts/New-ModulePackage.ps1`. Both
carry the label `5.0.0-rc7` and one assembly version, so every run used a
new process. All 11 files of each tested module folder equal the extracted
`NTFSSecurity.zip` byte for byte (SHA-256). The first packaging attempt, at
20:41 UTC, stopped in both builds at the check of the build script that
compares the package folder with the extracted zip ("The extracted ZIP
differs from the module folder"); the logs of that attempt are kept. I
changed the script (20:43) and built both again; the files that the lab
tested are those of the second attempt, and the cause of the first
mismatch wasn't recorded.
- Test source, identical in both runs (last written 20:56 and 20:54 UTC,
before the candidate run started at 21:02): `NTFSSecurity.Live.Tests.ps1`
(Git blob `67b85efeb45af67070538f241c203c4afa38b6f4`) and
`Invoke-NTFSSecurityLabTest.ps1` (blob
`0b46427bc32b0b15449e283a2a6cf67879937541`). Both are in the commit that
adds this record.
## Tests added
Case 10 adds 78 tests per edition to the 166 of the acceptance at `3442194`
(244 in all): 75 in the roles on the client and 3 for the state that the file
server finds. The fixture adds the folder `Case10` with delegated Full
Control, the folder `Locked` that Administrators own, and files that
Administrators own for the cases of `Set-NTFSOwner`.
| Describe (roles) | Tests | Fail on baseline | Fail on candidate | Fix |
| --- | ---: | ---: | ---: | --- |
| An item that the account owns and whose owner may not change its permissions (Delegate) | 2 | 2 | 0 | `c7a0383` owner restore |
| InheritedFrom of access entries that Windows cannot resolve (3 roles) | 3 | 3 | 0 | `2909a1c` |
| InheritedFrom of audit entries that Windows cannot resolve (ServerAdmin, Admin) | 2 | 2 | 0 | `2909a1c` |
| InheritedFrom of an item below a folder whose permissions the account cannot read (Delegate) | 2 | 1 | 0 | `2909a1c` |
| A later command that ends the pipeline or throws, for the item cmdlets (3 roles; 16 each) | 48 | 48 | 0 | `c77ecbf`, `40bf6a8` |
| A later command and the error of a folder that Get-ChildItem2 cannot read (Delegate) | 3 | 2 | 0 | `c77ecbf` (break), `d44a200` (throw) |
| Get-ChildItem2 -Filter (3 roles; brackets, `*.*`, null) | 9 | 9 | 0 | `ee7c105`, `40bf6a8`, `ae3078f` |
| Privileges when a later command takes the debug messages (Delegate, Admin) | 6 | 4 | 0 | `d44a200` |
| State of the file server: only the first item changed (Server; one per role) | 3 | 3 | 0 | `c77ecbf`, `40bf6a8` |
The tests that pass on the baseline are controls (a precondition, or the
privileges the cmdlets hold). `b14c90b` (public object APIs) has no lab
scenario; the package smoke below runs its unit tests. The fix of the leaked
native buffer has no observable guard.
A first run of the new tests on the candidate in Windows PowerShell failed
five tests. All five were errors of the tests, not of the module: a native
`icacls` call that Pester's `Stop` turned into a terminating error, and
assertions that expected a descriptor to be written at a verbose stop, which
that stop prevents. The tests were corrected, and the runs below are complete
runs of the final test files.
## Package smoke
Before the lab run, the eight unit-test files that guard the fixes ran
against the extracted candidate package in a scratch tree, in the four
configurations of the report (650 cases each): elevated Desktop 643 passed,
elevated Core 642, basic Desktop 528, basic Core 527; none failed; 7, 8, 122,
and 123 were skipped by their own conditions, which the report's eligibility
check covers.
## Lab and rollback evidence
`WindowsAccessControlLab`: F1ADC1, F1BDC1, F2DC1, F3DC1, F1AFile1 (client),
and F1AFile2 (file server), all Windows Server 2025 (10.0.26100). At
20:41 UTC, authenticated WinRM, LDAP RootDSE, Kerberos tickets, member secure
channels, and clocks (skew at most 7 s) passed on all six machines. At 20:43
UTC, before the first test run, no `NTFSSecurityLive` OU or `NtfsLive*`
account existed. The runs changed no VM, operating system, or network
setting. A process listing at the start showed no other controller of these
tests on the host; it wasn't kept as a log.
Six checkpoints named `ntfs-qg-paths-83149ee-before-acceptance` were taken
at 20:45 to 20:46 UTC, one per machine; the Hyper-V listing that shows the
names is kept with the evidence. The policy of each machine is Production,
but Hyper-V reports the type Standard. As before, Production classification
is unverified, and no checkpoint was restored or deleted. Every machine now
carries seven checkpoints of the acceptances since 2026-10-08, F1AFile1 eight.
## Live results
Candidate run 21:02 to 21:19 UTC, baseline run 21:22 to 21:39 UTC, each in
Windows PowerShell 5.1 and PowerShell 7 against the extracted package. Both
editions gave the same counts in each build.
| Build | Role | Passed | Failed | Skipped |
| --- | --- | ---: | ---: | ---: |
| Candidate | Delegate | 69 | 0 | 0 |
| Candidate | ServerAdmin | 34 | 0 | 0 |
| Candidate | Admin | 64 | 0 | 0 |
| Candidate | Server | 76 | 0 | 1 |
| Baseline | Delegate | 42 | 27 | 0 |
| Baseline | ServerAdmin | 13 | 21 | 0 |
| Baseline | Admin | 41 | 23 | 0 |
| Baseline | Server | 73 | 3 | 1 |
Candidate, both editions: 486 passed, zero failed, two skipped; the skip is
the test that needs the module in the Server role, which doesn't import it.
Baseline, both editions: 338 passed, 148 failed, two skipped. Every role
exited 0 on the candidate. A joined verification of the result files (not of
the counts) found the same 488 tests in both builds, no duplicate, and every
one of the 148 baseline failures passed on the candidate. The 148 failures
are 74 tests in each edition, all among the 78 new tests of each edition
(case 10 and the state test); the four that pass on both builds are
preconditions. [The results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv)
lists the 156 results (78 tests in two editions) with both outcomes and the
first line of the baseline message.
What the baseline shows, from its messages:
- Owner: `RestoreOwnerError ... (5) Access is denied` for the unchanged owner.
- `InheritedFrom`: a text of 13 characters instead of the 14 of
`unknown parent`.
- Later command: the `Downstream failure` of a `throw` never reached the
caller (the messages read `Expected like wildcard '*Downstream failure*' to
match $null`), and a `break` of a later command didn't leave the caller's
loop. For `Select-Object -First 1`, see the next section.
- `-Filter`: no result for a name with brackets; `*.*` returned only the
three names with a dot and dropped `NoExtension` and `NoExtensionFolder`;
`$null` gave `ArgumentNull` instead of the parameter validation error.
- Privileges: `TakeOwnership` still enabled after the pipeline stopped.
### Baseline failures without a message
Seven tests of each role, 21 per edition and 42 in all, fail on the baseline
with an empty message, and Pester prints no line for them: `Select-Object
-First 1` for the five item cmdlets, the verbose stop of
`Set-NTFSSecurityDescriptor`, and the debug stop of `Set-NTFSOwner`. This lab
run doesn't show what the baseline did in them. The State test of the Server
role shows it only for `Remove-Item2`: in each role, the second item was
removed after `Select-Object -First 1`. That test stops at its first failed
assertion, so it says nothing about the other cmdlets, and its assertions for
the debug and verbose stops check only that the second item is as it was,
which is also true when the client test never ran.
To close the gap, the bodies of these tests ran afterwards on this host, in a
sandbox below TEMP, with the settings of the runner (Pester 5.7.1,
`ErrorActionPreference` Stop), one build in one edition per process
(`Acceptance\Probe-LaterCommand.ps1`; it isn't part of the acceptance, and
it didn't run on a share). The result is the same in Windows PowerShell 5.1
and PowerShell 7:
| Cmdlet | Baseline `f11ff41`, after `Select-Object -First 1` and after `throw` | Candidate `83149ee` |
| --- | --- | --- |
| `Remove-Item2` | both items removed | the second item stays |
| `Copy-Item2` | both items copied | only the first is copied |
| `Move-Item2` | both items moved | the second item stays |
| `Set-NTFSOwner` | both owners changed, also at the debug stop | the second owner stays Administrators |
| `Set-NTFSSecurityDescriptor` | both descriptors written | only the first is written |
All 12 tests of the probe (seven stop rows, five `throw` rows) fail on the
baseline, the seven stop rows with no error record, as in the lab, and the
`throw` rows with the message of the lab; all 12 pass on the candidate. At the
verbose stop of `Set-NTFSSecurityDescriptor`, neither build writes a
descriptor, because the stop comes before the first write, so that failure on
the baseline isn't a change of state.
## Cleanup and review
Before the removal, the SIDs of the fixture were saved from the four domains
(10: seven in `a.forest1.net`, one each in `b.forest1.net`, `forest2.net`,
and `forest3.net`). The fixture was removed at 21:40 to 21:41 UTC with
`Invoke-NTFSSecurityLabTest.ps1 -RemoveFixture`. A separate read-only check
at 21:41 UTC, not the wrapper's marker, found in all four domains no
`NTFSSecurityLive` OU and no `NtfsLive*` account, and on F1AFile1 and
F1AFile2 no share, no `C:\NTFSSecurityLive` or `C:\NTFSSecurityLab`, no
`NtfsLiveLocal` group, no fixture member of Administrators, Access Control
Assistance Operators, or Remote Management Users, and no profile of the ten
SIDs. No checkpoint was restored.
One independent, read-only, static review of the finished change (tests,
fixture, README, this record and its results file, and Decision 22) ran
before the first commit. The custom `security-reviewer` can't start because
its configured model is unavailable, so the built-in code-review agent did
it. Verdict: approve with Minor; no Blocker and no Major. It confirmed that
the new tests can't pass vacuously (every precondition is asserted, the data
rows are not empty, nothing is shared between rows), that the fixture stays
below the guarded folders and throws when Administrators don't own the
files, and that the counts, the hashes, the 156 results, and the cleanup
facts of this record match the evidence. Its findings, all corrected in the
commit that follows the first: the fix that this record credited for the
`break` row, the claims about the State test and the 42 messageless
failures (now the section above, with the diagnostic), this heading, the
count of results, the wording about the folders before the run, the
truncated messages in the results file, the README row of case 10, and the
migration hint of item 8 and the comparison with `Copy-Item` in Decision 22.
It could not run anything, so the run state and the lab-wide claims rest on
the logs; the diagnostic above and the checkpoint listing close two of its
open points.
## Limits
- `Get-NTFSAudit` below an unreadable parent folder can't be built here: an
account that may read the audit entries (it holds the Security privilege)
also reads the DACL of an Administrators-owned folder. The audit scenario
uses a file that was deleted after it was read, which reaches the same
`unknown parent` text.
- The run covers the candidate package from disk (`-ModulePath`), not the
published package, one lab, and Windows Server 2025 only. The other
operating systems of Decision 21, the acceptance of the published
prerelease, and the answer of a non-Windows file server (#34) stay with the
other gates. The stable version remains 4.2.6.
- The candidate and the baseline differ only by the 28 commits; the test and
controller files are the same.
- What the baseline did in the 42 failures without a message is shown by a
local diagnostic, not by this lab run. A State test split per cmdlet and
stop style would show it on the share too, and would need both lab runs
again.
## Evidence
The result files, logs, hashes, readiness, checkpoint, snapshot, and cleanup
logs of both runs are in the session artifact
`4b12e2f4-d4c7-4a5d-883a-ddb7421c4848\files\lab-qg-paths` (local, not in Git);
so are the Hyper-V listing of the checkpoint names
(`checkpoints-83149ee-names.csv`) and the outputs of the diagnostic
(`runs\diagnostic-mute`, and `runs\diagnostic-mute-first-run` from before the
probe listed owners and entries). The per-test results of case 10 are in
[the results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv). The
scripts that build, package, run, and clean up in this acceptance contain
paths of the session folder and stay in the session artifact; the generic
ones that it used, `Validate-LabResults.ps1` (the check of the result files)
and `Probe-LaterCommand.ps1` (the diagnostic), are in the folder
[Acceptance](Acceptance).

129
Tests/Lab/Acceptance-2026-10-10-os-matrix-Cells.csv

@ -0,0 +1,129 @@
"Run","Candidate","FileServer","FileServerOs","Client","ClientOs","Edition","Role","Account","ExitCode","Passed","Failed","Skipped"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7c","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","34","0","0"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7e","83149ee","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7f","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","1","50","1","1"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","1","50","1","1"
"rc7f","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7g","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","1","50","1","1"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","1","50","1","1"
"rc7h","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","1","50","1","1"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7i","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","1","34","2","0"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","1","49","2","1"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","1","34","2","0"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","1","49","2","1"
"rc7j","962887a","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","1","34","2","0"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","1","34","2","0"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7k","83149ee","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile19","Windows Server 2019 Datacenter 10.0.17763.1217 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile22","Windows Server 2022 Datacenter 10.0.20348.4773 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Desktop","Server","osmatrix\install","0","73","0","1"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Delegate","osmatrix\NtfsLiveDelegate","0","69","0","0"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","ServerAdmin","osmatrix\NtfsLiveServerAdmin","0","36","0","0"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Admin","osmatrix\NtfsLiveAdmin","0","51","0","1"
"rc7l","fdd7a8b","OSFile25","Windows Server 2025 Datacenter 10.0.26100.32690 (server)","OSWin11E","Windows 11 Enterprise Evaluation 10.0.22621.525 (client)","Core","Server","osmatrix\install","0","73","0","1"
1 Run Candidate FileServer FileServerOs Client ClientOs Edition Role Account ExitCode Passed Failed Skipped
2 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
3 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
4 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
5 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
6 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
7 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
8 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
9 rc7c 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
10 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
11 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
12 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
13 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
14 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
15 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
16 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
17 rc7c 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
18 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
19 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
20 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
21 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
22 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
23 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
24 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
25 rc7c 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
26 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
27 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
28 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
29 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
30 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
31 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
32 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
33 rc7e 83149ee OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
34 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
35 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
36 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
37 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
38 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
39 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
40 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
41 rc7e 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
42 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
43 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
44 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
45 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
46 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
47 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 34 0 0
48 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
49 rc7e 83149ee OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
50 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
51 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
52 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
53 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
54 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
55 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
56 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
57 rc7f fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
58 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
59 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
60 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 1 50 1 1
61 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
62 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
63 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
64 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 1 50 1 1
65 rc7f fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
66 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
67 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
68 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
69 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
70 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
71 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
72 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
73 rc7g fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
74 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
75 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
76 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 1 50 1 1
77 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
78 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
79 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
80 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 1 50 1 1
81 rc7h fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
82 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
83 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
84 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 1 50 1 1
85 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
86 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
87 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
88 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
89 rc7i fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
90 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
91 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 1 34 2 0
92 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 1 49 2 1
93 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
94 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
95 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 1 34 2 0
96 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 1 49 2 1
97 rc7j 962887a OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
98 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
99 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 1 34 2 0
100 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
101 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
102 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
103 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 1 34 2 0
104 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
105 rc7k 83149ee OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
106 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
107 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
108 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
109 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
110 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
111 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
112 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
113 rc7l fdd7a8b OSFile19 Windows Server 2019 Datacenter 10.0.17763.1217 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
114 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
115 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
116 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
117 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
118 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
119 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
120 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
121 rc7l fdd7a8b OSFile22 Windows Server 2022 Datacenter 10.0.20348.4773 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1
122 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
123 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
124 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Admin osmatrix\NtfsLiveAdmin 0 51 0 1
125 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Desktop Server osmatrix\install 0 73 0 1
126 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Delegate osmatrix\NtfsLiveDelegate 0 69 0 0
127 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core ServerAdmin osmatrix\NtfsLiveServerAdmin 0 36 0 0
128 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Admin osmatrix\NtfsLiveAdmin 0 51 0 1
129 rc7l fdd7a8b OSFile25 Windows Server 2025 Datacenter 10.0.26100.32690 (server) OSWin11E Windows 11 Enterprise Evaluation 10.0.22621.525 (client) Core Server osmatrix\install 0 73 0 1

297
Tests/Lab/Acceptance-2026-10-10-os-matrix-Failures.csv

@ -0,0 +1,297 @@
"Candidate","Machine","Mode","Edition","Test"
"83149ee","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"83149ee","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"83149ee","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"83149ee","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"83149ee","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"83149ee","OSFile22","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"83149ee","OSFile22","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE22 and warn no more than for localhost"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"83149ee","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"83149ee","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"83149ee","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"83149ee","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"83149ee","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"83149ee","OSFile22","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"83149ee","OSFile22","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22 and warn no more than for localhost"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"83149ee","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile22","Elevated","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile22","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries"
"83149ee","OSFile22","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries"
"83149ee","OSFile22","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item"
"83149ee","OSFile22","Elevated","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile22","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries"
"83149ee","OSFile22","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries"
"83149ee","OSFile22","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item"
"83149ee","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"83149ee","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"83149ee","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"83149ee","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"83149ee","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"83149ee","OSFile25","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"83149ee","OSFile25","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE25 and warn no more than for localhost"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"83149ee","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"83149ee","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"83149ee","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"83149ee","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"83149ee","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"83149ee","OSFile25","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"83149ee","OSFile25","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25 and warn no more than for localhost"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"83149ee","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile25","Elevated","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile25","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries"
"83149ee","OSFile25","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries"
"83149ee","OSFile25","Elevated","Core","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item"
"83149ee","OSFile25","Elevated","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"83149ee","OSFile25","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries"
"83149ee","OSFile25","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries"
"83149ee","OSFile25","Elevated","Desktop","Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item"
"962887a","OSFile19","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile19","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile19","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile19","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile19","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile19","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile19","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE19 and warn no more than for localhost"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19.osmatrix.net and warn no more than for localhost"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile19","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSFile19","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile19","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile19","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile19","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile19","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile19","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile19","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19 and warn no more than for localhost"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19.osmatrix.net and warn no more than for localhost"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile19","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile22","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile22","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile22","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE22 and warn no more than for localhost"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile22","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile22","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile22","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile22","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22 and warn no more than for localhost"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile22","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile25","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile25","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile25","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE25 and warn no more than for localhost"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile25","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSFile25","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSFile25","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSFile25","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25 and warn no more than for localhost"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSFile25","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSWin11","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSWin11","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSWin11","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSWin11","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSWin11","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSWin11","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSWin11","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWIN11 and warn no more than for localhost"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11.osmatrix.net and warn no more than for localhost"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSWin11","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSWin11","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSWin11","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSWin11","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSWin11","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSWin11","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSWin11","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSWin11","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11 and warn no more than for localhost"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11.osmatrix.net and warn no more than for localhost"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSWin11","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSWin11E","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSWin11E","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSWin11E","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSWin11E","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSWin11E","Basic","Core","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSWin11E","Basic","Core","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSWin11E","Basic","Core","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWIN11E and warn no more than for localhost"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E.osmatrix.net and warn no more than for localhost"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSWin11E","Basic","Core","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
"962887a","OSWin11E","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else"
"962887a","OSWin11E","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else"
"962887a","OSWin11E","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else"
"962887a","OSWin11E","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else"
"962887a","OSWin11E","Basic","Desktop","A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else"
"962887a","OSWin11E","Basic","Desktop","A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path"
"962887a","OSWin11E","Basic","Desktop","An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E and warn no more than for localhost"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E.osmatrix.net and warn no more than for localhost"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn"
"962887a","OSWin11E","Basic","Desktop","Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName"
1 Candidate Machine Mode Edition Test
2 83149ee OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
3 83149ee OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
4 83149ee OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
5 83149ee OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
6 83149ee OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
7 83149ee OSFile22 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
8 83149ee OSFile22 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
9 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
10 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
11 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
12 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
13 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
14 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
15 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
16 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
17 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
18 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE22 and warn no more than for localhost
19 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost
20 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
21 83149ee OSFile22 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
22 83149ee OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
23 83149ee OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
24 83149ee OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
25 83149ee OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
26 83149ee OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
27 83149ee OSFile22 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
28 83149ee OSFile22 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
29 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
30 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
31 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
32 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
33 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
34 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
35 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
36 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
37 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
38 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22 and warn no more than for localhost
39 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost
40 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
41 83149ee OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
42 83149ee OSFile22 Elevated Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
43 83149ee OSFile22 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries
44 83149ee OSFile22 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries
45 83149ee OSFile22 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item
46 83149ee OSFile22 Elevated Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
47 83149ee OSFile22 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries
48 83149ee OSFile22 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries
49 83149ee OSFile22 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item
50 83149ee OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
51 83149ee OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
52 83149ee OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
53 83149ee OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
54 83149ee OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
55 83149ee OSFile25 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
56 83149ee OSFile25 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
57 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
58 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
59 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
60 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
61 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
62 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
63 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
64 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
65 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
66 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE25 and warn no more than for localhost
67 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost
68 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
69 83149ee OSFile25 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
70 83149ee OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
71 83149ee OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
72 83149ee OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
73 83149ee OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
74 83149ee OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
75 83149ee OSFile25 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
76 83149ee OSFile25 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
77 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
78 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
79 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
80 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
81 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
82 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
83 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
84 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
85 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
86 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25 and warn no more than for localhost
87 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost
88 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
89 83149ee OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
90 83149ee OSFile25 Elevated Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
91 83149ee OSFile25 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries
92 83149ee OSFile25 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries
93 83149ee OSFile25 Elevated Core Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item
94 83149ee OSFile25 Elevated Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
95 83149ee OSFile25 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a file without audit entries
96 83149ee OSFile25 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of a folder without audit entries
97 83149ee OSFile25 Elevated Desktop Get-NTFSInheritance.With a security descriptor.Should report the same state as for the path of the item
98 962887a OSFile19 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
99 962887a OSFile19 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
100 962887a OSFile19 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
101 962887a OSFile19 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
102 962887a OSFile19 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
103 962887a OSFile19 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
104 962887a OSFile19 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
105 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
106 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
107 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
108 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
109 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
110 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
111 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
112 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
113 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
114 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE19 and warn no more than for localhost
115 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19.osmatrix.net and warn no more than for localhost
116 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
117 962887a OSFile19 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
118 962887a OSFile19 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
119 962887a OSFile19 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
120 962887a OSFile19 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
121 962887a OSFile19 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
122 962887a OSFile19 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
123 962887a OSFile19 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
124 962887a OSFile19 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
125 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
126 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
127 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
128 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
129 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
130 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
131 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
132 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
133 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
134 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19 and warn no more than for localhost
135 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile19.osmatrix.net and warn no more than for localhost
136 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
137 962887a OSFile19 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
138 962887a OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
139 962887a OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
140 962887a OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
141 962887a OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
142 962887a OSFile22 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
143 962887a OSFile22 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
144 962887a OSFile22 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
145 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
146 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
147 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
148 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
149 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
150 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
151 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
152 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
153 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
154 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE22 and warn no more than for localhost
155 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost
156 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
157 962887a OSFile22 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
158 962887a OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
159 962887a OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
160 962887a OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
161 962887a OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
162 962887a OSFile22 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
163 962887a OSFile22 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
164 962887a OSFile22 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
165 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
166 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
167 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
168 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
169 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
170 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
171 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
172 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
173 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
174 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22 and warn no more than for localhost
175 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile22.osmatrix.net and warn no more than for localhost
176 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
177 962887a OSFile22 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
178 962887a OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
179 962887a OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
180 962887a OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
181 962887a OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
182 962887a OSFile25 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
183 962887a OSFile25 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
184 962887a OSFile25 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
185 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
186 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
187 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
188 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
189 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
190 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
191 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
192 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
193 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
194 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFILE25 and warn no more than for localhost
195 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost
196 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
197 962887a OSFile25 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
198 962887a OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
199 962887a OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
200 962887a OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
201 962887a OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
202 962887a OSFile25 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
203 962887a OSFile25 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
204 962887a OSFile25 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
205 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
206 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
207 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
208 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
209 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
210 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
211 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
212 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
213 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
214 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25 and warn no more than for localhost
215 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSFile25.osmatrix.net and warn no more than for localhost
216 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
217 962887a OSFile25 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
218 962887a OSWin11 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
219 962887a OSWin11 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
220 962887a OSWin11 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
221 962887a OSWin11 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
222 962887a OSWin11 Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
223 962887a OSWin11 Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
224 962887a OSWin11 Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
225 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
226 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
227 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
228 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
229 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
230 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
231 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
232 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
233 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
234 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWIN11 and warn no more than for localhost
235 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11.osmatrix.net and warn no more than for localhost
236 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
237 962887a OSWin11 Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
238 962887a OSWin11 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
239 962887a OSWin11 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
240 962887a OSWin11 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
241 962887a OSWin11 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
242 962887a OSWin11 Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
243 962887a OSWin11 Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
244 962887a OSWin11 Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
245 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
246 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
247 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
248 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
249 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
250 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
251 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
252 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
253 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
254 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11 and warn no more than for localhost
255 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11.osmatrix.net and warn no more than for localhost
256 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
257 962887a OSWin11 Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
258 962887a OSWin11E Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
259 962887a OSWin11E Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
260 962887a OSWin11E Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
261 962887a OSWin11E Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
262 962887a OSWin11E Basic Core A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
263 962887a OSWin11E Basic Core A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
264 962887a OSWin11E Basic Core An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
265 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
266 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
267 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
268 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
269 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
270 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
271 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
272 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
273 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
274 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWIN11E and warn no more than for localhost
275 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E.osmatrix.net and warn no more than for localhost
276 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
277 962887a OSWin11E Basic Core Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName
278 962887a OSWin11E Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for break after its first object and change nothing else
279 962887a OSWin11E Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should leave the loop for continue after its first object and change nothing else
280 962887a OSWin11E Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop after the first object for Select-Object -First 1 and change nothing else
281 962887a OSWin11E Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (throw) of the later command and change nothing else
282 962887a OSWin11E Basic Desktop A later command that ends the pipeline.Get-NTFSEffectiveAccess should stop for a terminating error (Write-Error -ErrorAction Stop) of the later command and change nothing else
283 962887a OSWin11E Basic Desktop A path that does not exist.Get-NTFSEffectiveAccess should write a ReadFileError for it and continue with the next path
284 962887a OSWin11E Basic Desktop An item whose owner may not read its permissions.Get-NTFSEffectiveAccess should write a ReadSecurityError for it and continue with the next path
285 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each Path and return no access entry
286 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess for an unresolved identity.Should report the native identity error for each SecurityDescriptor and return no access entry
287 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.Should compute the effective access of a security descriptor
288 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.Should leave out an account without access when -ExcludeNoneAccessEntries is used
289 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.Should return an account with access when -ExcludeNoneAccessEntries is used
290 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.Should use the current location when -Path is omitted
291 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.Should warn once that the Security privilege is missing
292 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for . and warn no more than for localhost
293 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for LOCALHOST and warn no more than for localhost
294 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E and warn no more than for localhost
295 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When -ServerName names this computer.Should return the result of localhost for OSWin11E.osmatrix.net and warn no more than for localhost
296 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn
297 962887a OSWin11E Basic Desktop Get-NTFSEffectiveAccess.When the computer of -ServerName cannot be reached.Should return the result of this computer and warn for an empty -ServerName

9
Tests/Lab/Acceptance-2026-10-10-os-matrix-FirstLab.csv

@ -0,0 +1,9 @@
"Version","Edition","Role","Account","ExitCode","Passed","Failed","Skipped"
"local","Desktop","Delegate","A\NtfsLiveDelegate","0","69","0","0"
"local","Desktop","ServerAdmin","A\NtfsLiveServerAdmin","0","36","0","0"
"local","Desktop","Admin","A\NtfsLiveAdmin","0","64","0","0"
"local","Desktop","Server","A\install","0","76","0","1"
"local","Core","Delegate","A\NtfsLiveDelegate","0","69","0","0"
"local","Core","ServerAdmin","A\NtfsLiveServerAdmin","0","36","0","0"
"local","Core","Admin","A\NtfsLiveAdmin","0","64","0","0"
"local","Core","Server","A\install","0","76","0","1"
1 Version Edition Role Account ExitCode Passed Failed Skipped
2 local Desktop Delegate A\NtfsLiveDelegate 0 69 0 0
3 local Desktop ServerAdmin A\NtfsLiveServerAdmin 0 36 0 0
4 local Desktop Admin A\NtfsLiveAdmin 0 64 0 0
5 local Desktop Server A\install 0 76 0 1
6 local Core Delegate A\NtfsLiveDelegate 0 69 0 0
7 local Core ServerAdmin A\NtfsLiveServerAdmin 0 36 0 0
8 local Core Admin A\NtfsLiveAdmin 0 64 0 0
9 local Core Server A\install 0 76 0 1

57
Tests/Lab/Acceptance-2026-10-10-os-matrix-LocalSuite.csv

@ -0,0 +1,57 @@
"Candidate","Machine","Os","Mode","Edition","PowerShell","Result","Passed","Failed","Skipped","Total","Seconds"
"83149ee","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Core","7.6.3","Failed","760","20","231","1011","109"
"83149ee","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Desktop","5.1.20348.4294","Failed","762","20","229","1011","121"
"83149ee","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Core","7.6.3","Failed","988","4","19","1011","230"
"83149ee","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Desktop","5.1.20348.4294","Failed","990","4","17","1011","125"
"83149ee","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Core","7.6.3","Failed","760","20","231","1011","118"
"83149ee","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Desktop","5.1.26100.32684","Failed","762","20","229","1011","114"
"83149ee","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Core","7.6.3","Failed","988","4","19","1011","238"
"83149ee","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Desktop","5.1.26100.32684","Failed","990","4","17","1011","259"
"962887a","LOCAL","Windows Server 2025 Datacenter 26100.33438","Basic","Core","7.6.6","Passed","779","0","231","1010","20"
"962887a","LOCAL","Windows Server 2025 Datacenter 26100.33438","Basic","Desktop","5.1.26100.33438","Passed","781","0","229","1010","22"
"962887a","LOCAL","Windows Server 2025 Datacenter 26100.33438","Elevated","Core","7.6.6","Passed","991","0","19","1010","91"
"962887a","LOCAL","Windows Server 2025 Datacenter 26100.33438","Elevated","Desktop","5.1.26100.33438","Passed","993","0","17","1010","97"
"962887a","OSFile19","Windows Server 2019 Datacenter 17763.1217","Basic","Core","7.6.3","Failed","760","20","231","1011","111"
"962887a","OSFile19","Windows Server 2019 Datacenter 17763.1217","Basic","Desktop","5.1.17763.1007","Failed","762","20","229","1011","120"
"962887a","OSFile19","Windows Server 2019 Datacenter 17763.1217","Elevated","Core","7.6.3","Passed","992","0","19","1011","205"
"962887a","OSFile19","Windows Server 2019 Datacenter 17763.1217","Elevated","Desktop","5.1.17763.1007","Passed","994","0","17","1011","96"
"962887a","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Core","7.6.3","Failed","760","20","231","1011","120"
"962887a","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Desktop","5.1.20348.4294","Failed","762","20","229","1011","117"
"962887a","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Core","7.6.3","Passed","992","0","19","1011","230"
"962887a","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Desktop","5.1.20348.4294","Passed","994","0","17","1011","123"
"962887a","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Core","7.6.3","Failed","760","20","231","1011","108"
"962887a","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Desktop","5.1.26100.32684","Failed","762","20","229","1011","114"
"962887a","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Core","7.6.3","Passed","992","0","19","1011","226"
"962887a","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Desktop","5.1.26100.32684","Passed","994","0","17","1011","256"
"962887a","OSWin11","Windows 10 Pro 28000.1836","Basic","Core","7.6.3","Failed","760","20","231","1011","30"
"962887a","OSWin11","Windows 10 Pro 28000.1836","Basic","Desktop","5.1.28000.1830","Failed","762","20","229","1011","120"
"962887a","OSWin11","Windows 10 Pro 28000.1836","Elevated","Core","7.6.3","Passed","992","0","19","1011","104"
"962887a","OSWin11","Windows 10 Pro 28000.1836","Elevated","Desktop","5.1.28000.1830","Passed","994","0","17","1011","170"
"962887a","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Basic","Core","7.6.3","Failed","760","20","231","1011","216"
"962887a","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Basic","Desktop","5.1.22621.169","Failed","762","20","229","1011","347"
"962887a","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Elevated","Core","7.6.3","Passed","992","0","19","1011","403"
"962887a","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Elevated","Desktop","5.1.22621.169","Passed","994","0","17","1011","431"
"fdd7a8b","LOCAL","Windows Server 2025 Datacenter 26100.33438","Basic","Core","7.6.6","Passed","779","0","231","1010","20"
"fdd7a8b","LOCAL","Windows Server 2025 Datacenter 26100.33438","Basic","Desktop","5.1.26100.33438","Passed","781","0","229","1010","23"
"fdd7a8b","LOCAL","Windows Server 2025 Datacenter 26100.33438","Elevated","Core","7.6.6","Passed","991","0","19","1010","92"
"fdd7a8b","LOCAL","Windows Server 2025 Datacenter 26100.33438","Elevated","Desktop","5.1.26100.33438","Passed","993","0","17","1010","104"
"fdd7a8b","OSFile19","Windows Server 2019 Datacenter 17763.1217","Basic","Core","7.6.3","Passed","780","0","231","1011","99"
"fdd7a8b","OSFile19","Windows Server 2019 Datacenter 17763.1217","Basic","Desktop","5.1.17763.1007","Passed","782","0","229","1011","108"
"fdd7a8b","OSFile19","Windows Server 2019 Datacenter 17763.1217","Elevated","Core","7.6.3","Passed","992","0","19","1011","197"
"fdd7a8b","OSFile19","Windows Server 2019 Datacenter 17763.1217","Elevated","Desktop","5.1.17763.1007","Passed","994","0","17","1011","101"
"fdd7a8b","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Core","7.6.3","Passed","780","0","231","1011","115"
"fdd7a8b","OSFile22","Windows Server 2022 Datacenter 20348.4773","Basic","Desktop","5.1.20348.4294","Passed","782","0","229","1011","130"
"fdd7a8b","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Core","7.6.3","Passed","992","0","19","1011","213"
"fdd7a8b","OSFile22","Windows Server 2022 Datacenter 20348.4773","Elevated","Desktop","5.1.20348.4294","Passed","994","0","17","1011","127"
"fdd7a8b","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Core","7.6.3","Passed","780","0","231","1011","120"
"fdd7a8b","OSFile25","Windows Server 2025 Datacenter 26100.32690","Basic","Desktop","5.1.26100.32684","Passed","782","0","229","1011","112"
"fdd7a8b","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Core","7.6.3","Passed","992","0","19","1011","219"
"fdd7a8b","OSFile25","Windows Server 2025 Datacenter 26100.32690","Elevated","Desktop","5.1.26100.32684","Passed","994","0","17","1011","257"
"fdd7a8b","OSWin11","Windows 10 Pro 28000.1836","Basic","Core","7.6.3","Passed","780","0","231","1011","37"
"fdd7a8b","OSWin11","Windows 10 Pro 28000.1836","Basic","Desktop","5.1.28000.1830","Passed","782","0","229","1011","126"
"fdd7a8b","OSWin11","Windows 10 Pro 28000.1836","Elevated","Core","7.6.3","Passed","992","0","19","1011","104"
"fdd7a8b","OSWin11","Windows 10 Pro 28000.1836","Elevated","Desktop","5.1.28000.1830","Passed","994","0","17","1011","180"
"fdd7a8b","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Basic","Core","7.6.3","Passed","780","0","231","1011","33"
"fdd7a8b","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Basic","Desktop","5.1.22621.169","Passed","782","0","229","1011","167"
"fdd7a8b","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Elevated","Core","7.6.3","Passed","992","0","19","1011","113"
"fdd7a8b","OSWin11E","Windows 10 Enterprise Evaluation 22621.525","Elevated","Desktop","5.1.22621.169","Passed","994","0","17","1011","132"
1 Candidate Machine Os Mode Edition PowerShell Result Passed Failed Skipped Total Seconds
2 83149ee OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Core 7.6.3 Failed 760 20 231 1011 109
3 83149ee OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Desktop 5.1.20348.4294 Failed 762 20 229 1011 121
4 83149ee OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Core 7.6.3 Failed 988 4 19 1011 230
5 83149ee OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Desktop 5.1.20348.4294 Failed 990 4 17 1011 125
6 83149ee OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Core 7.6.3 Failed 760 20 231 1011 118
7 83149ee OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Desktop 5.1.26100.32684 Failed 762 20 229 1011 114
8 83149ee OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Core 7.6.3 Failed 988 4 19 1011 238
9 83149ee OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Desktop 5.1.26100.32684 Failed 990 4 17 1011 259
10 962887a LOCAL Windows Server 2025 Datacenter 26100.33438 Basic Core 7.6.6 Passed 779 0 231 1010 20
11 962887a LOCAL Windows Server 2025 Datacenter 26100.33438 Basic Desktop 5.1.26100.33438 Passed 781 0 229 1010 22
12 962887a LOCAL Windows Server 2025 Datacenter 26100.33438 Elevated Core 7.6.6 Passed 991 0 19 1010 91
13 962887a LOCAL Windows Server 2025 Datacenter 26100.33438 Elevated Desktop 5.1.26100.33438 Passed 993 0 17 1010 97
14 962887a OSFile19 Windows Server 2019 Datacenter 17763.1217 Basic Core 7.6.3 Failed 760 20 231 1011 111
15 962887a OSFile19 Windows Server 2019 Datacenter 17763.1217 Basic Desktop 5.1.17763.1007 Failed 762 20 229 1011 120
16 962887a OSFile19 Windows Server 2019 Datacenter 17763.1217 Elevated Core 7.6.3 Passed 992 0 19 1011 205
17 962887a OSFile19 Windows Server 2019 Datacenter 17763.1217 Elevated Desktop 5.1.17763.1007 Passed 994 0 17 1011 96
18 962887a OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Core 7.6.3 Failed 760 20 231 1011 120
19 962887a OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Desktop 5.1.20348.4294 Failed 762 20 229 1011 117
20 962887a OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Core 7.6.3 Passed 992 0 19 1011 230
21 962887a OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Desktop 5.1.20348.4294 Passed 994 0 17 1011 123
22 962887a OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Core 7.6.3 Failed 760 20 231 1011 108
23 962887a OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Desktop 5.1.26100.32684 Failed 762 20 229 1011 114
24 962887a OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Core 7.6.3 Passed 992 0 19 1011 226
25 962887a OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Desktop 5.1.26100.32684 Passed 994 0 17 1011 256
26 962887a OSWin11 Windows 10 Pro 28000.1836 Basic Core 7.6.3 Failed 760 20 231 1011 30
27 962887a OSWin11 Windows 10 Pro 28000.1836 Basic Desktop 5.1.28000.1830 Failed 762 20 229 1011 120
28 962887a OSWin11 Windows 10 Pro 28000.1836 Elevated Core 7.6.3 Passed 992 0 19 1011 104
29 962887a OSWin11 Windows 10 Pro 28000.1836 Elevated Desktop 5.1.28000.1830 Passed 994 0 17 1011 170
30 962887a OSWin11E Windows 10 Enterprise Evaluation 22621.525 Basic Core 7.6.3 Failed 760 20 231 1011 216
31 962887a OSWin11E Windows 10 Enterprise Evaluation 22621.525 Basic Desktop 5.1.22621.169 Failed 762 20 229 1011 347
32 962887a OSWin11E Windows 10 Enterprise Evaluation 22621.525 Elevated Core 7.6.3 Passed 992 0 19 1011 403
33 962887a OSWin11E Windows 10 Enterprise Evaluation 22621.525 Elevated Desktop 5.1.22621.169 Passed 994 0 17 1011 431
34 fdd7a8b LOCAL Windows Server 2025 Datacenter 26100.33438 Basic Core 7.6.6 Passed 779 0 231 1010 20
35 fdd7a8b LOCAL Windows Server 2025 Datacenter 26100.33438 Basic Desktop 5.1.26100.33438 Passed 781 0 229 1010 23
36 fdd7a8b LOCAL Windows Server 2025 Datacenter 26100.33438 Elevated Core 7.6.6 Passed 991 0 19 1010 92
37 fdd7a8b LOCAL Windows Server 2025 Datacenter 26100.33438 Elevated Desktop 5.1.26100.33438 Passed 993 0 17 1010 104
38 fdd7a8b OSFile19 Windows Server 2019 Datacenter 17763.1217 Basic Core 7.6.3 Passed 780 0 231 1011 99
39 fdd7a8b OSFile19 Windows Server 2019 Datacenter 17763.1217 Basic Desktop 5.1.17763.1007 Passed 782 0 229 1011 108
40 fdd7a8b OSFile19 Windows Server 2019 Datacenter 17763.1217 Elevated Core 7.6.3 Passed 992 0 19 1011 197
41 fdd7a8b OSFile19 Windows Server 2019 Datacenter 17763.1217 Elevated Desktop 5.1.17763.1007 Passed 994 0 17 1011 101
42 fdd7a8b OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Core 7.6.3 Passed 780 0 231 1011 115
43 fdd7a8b OSFile22 Windows Server 2022 Datacenter 20348.4773 Basic Desktop 5.1.20348.4294 Passed 782 0 229 1011 130
44 fdd7a8b OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Core 7.6.3 Passed 992 0 19 1011 213
45 fdd7a8b OSFile22 Windows Server 2022 Datacenter 20348.4773 Elevated Desktop 5.1.20348.4294 Passed 994 0 17 1011 127
46 fdd7a8b OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Core 7.6.3 Passed 780 0 231 1011 120
47 fdd7a8b OSFile25 Windows Server 2025 Datacenter 26100.32690 Basic Desktop 5.1.26100.32684 Passed 782 0 229 1011 112
48 fdd7a8b OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Core 7.6.3 Passed 992 0 19 1011 219
49 fdd7a8b OSFile25 Windows Server 2025 Datacenter 26100.32690 Elevated Desktop 5.1.26100.32684 Passed 994 0 17 1011 257
50 fdd7a8b OSWin11 Windows 10 Pro 28000.1836 Basic Core 7.6.3 Passed 780 0 231 1011 37
51 fdd7a8b OSWin11 Windows 10 Pro 28000.1836 Basic Desktop 5.1.28000.1830 Passed 782 0 229 1011 126
52 fdd7a8b OSWin11 Windows 10 Pro 28000.1836 Elevated Core 7.6.3 Passed 992 0 19 1011 104
53 fdd7a8b OSWin11 Windows 10 Pro 28000.1836 Elevated Desktop 5.1.28000.1830 Passed 994 0 17 1011 180
54 fdd7a8b OSWin11E Windows 10 Enterprise Evaluation 22621.525 Basic Core 7.6.3 Passed 780 0 231 1011 33
55 fdd7a8b OSWin11E Windows 10 Enterprise Evaluation 22621.525 Basic Desktop 5.1.22621.169 Passed 782 0 229 1011 167
56 fdd7a8b OSWin11E Windows 10 Enterprise Evaluation 22621.525 Elevated Core 7.6.3 Passed 992 0 19 1011 113
57 fdd7a8b OSWin11E Windows 10 Enterprise Evaluation 22621.525 Elevated Desktop 5.1.22621.169 Passed 994 0 17 1011 132

44
Tests/Lab/Acceptance-2026-10-10-os-matrix-Timeline.csv

@ -0,0 +1,44 @@
"Run","Candidate","FileServer","Edition","Subject","SubjectRid","SameNameAsPreviousCell","SameAccountAsPreviousCell","PreviousRemoval","AccountsCreated","AdminRoleStarted","MinutesRemovalToCreation","MinutesCreationToAdmin","MinutesRemovalToAdmin","T1ServerNameFileServer","T2DefaultServerName","T3UnreachableServerName","EffectiveAccessFailures"
"rc7c","83149ee","OSFile19","Desktop","osmatrix\NtfsLiveSubject","1110","False","False","","2026-10-09 23:43:43","2026-10-09 23:46:33","","2.8","","pass 276ms","pass 43ms","pass 12.1s","0"
"rc7c","83149ee","OSFile19","Core","osmatrix\NtfsLiveSubject","1110","False","False","","2026-10-09 23:43:43","2026-10-09 23:48:09","","4.4","","pass 165ms","pass 24ms","pass 11.06s","0"
"rc7c","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1110","True","True","","2026-10-09 23:49:38","2026-10-09 23:52:16","","2.6","","pass 439ms","pass 37ms","pass 11.47s","0"
"rc7c","83149ee","OSFile22","Core","osmatrix\NtfsLiveSubject","1110","True","True","","2026-10-09 23:49:38","2026-10-09 23:53:48","","4.2","","pass 164ms","pass 33ms","pass 11.76s","0"
"rc7c","83149ee","OSFile25","Desktop","osmatrix\NtfsLiveSubject","1110","True","True","","2026-10-09 23:55:37","2026-10-09 23:58:32","","2.9","","pass 423ms","pass 41ms","pass 11.95s","0"
"rc7c","83149ee","OSFile25","Core","osmatrix\NtfsLiveSubject","1110","True","True","","2026-10-09 23:55:37","2026-10-10 00:01:33","","5.9","","pass 129ms","pass 40ms","pass 11.58s","0"
"rc7e","83149ee","OSFile19","Desktop","osmatrix\NtfsLiveSubject","1130","True","False","2026-10-10 00:16:22","2026-10-10 00:20:59","2026-10-10 00:23:36","4.6","2.6","7.2","pass 250ms","pass 37ms","pass 11.55s","0"
"rc7e","83149ee","OSFile19","Core","osmatrix\NtfsLiveSubject","1130","True","False","2026-10-10 00:16:22","2026-10-10 00:20:59","2026-10-10 00:25:03","4.6","4.1","8.7","pass 147ms","pass 26ms","pass 11.16s","0"
"rc7e","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1130","True","True","","2026-10-10 00:26:33","2026-10-10 00:29:19","","2.8","","pass 445ms","pass 36ms","pass 12.11s","0"
"rc7e","83149ee","OSFile22","Core","osmatrix\NtfsLiveSubject","1130","True","True","","2026-10-10 00:26:33","2026-10-10 00:30:47","","4.2","","pass 158ms","pass 31ms","pass 12.09s","0"
"rc7e","83149ee","OSFile25","Desktop","osmatrix\NtfsLiveSubject","1141","True","False","2026-10-10 00:31:54","2026-10-10 00:32:55","2026-10-10 00:35:48","1.0","2.9","3.9","pass 256ms","pass 28ms","pass 12.11s","0"
"rc7e","83149ee","OSFile25","Core","osmatrix\NtfsLiveSubject","1141","True","False","2026-10-10 00:31:54","2026-10-10 00:32:55","2026-10-10 00:41:22","1.0","8.5","9.5","pass 137ms","pass 50ms","pass 11.22s","0"
"rc7f","fdd7a8b","OSFile19","Desktop","osmatrix\NtfsLiveSubject","1153","True","False","2026-10-10 00:44:09","2026-10-10 02:10:50","2026-10-10 02:13:46","86.7","2.9","89.6","pass 287ms","pass 44ms","pass 11.53s","0"
"rc7f","fdd7a8b","OSFile19","Core","osmatrix\NtfsLiveSubject","1153","True","False","2026-10-10 00:44:09","2026-10-10 02:10:50","2026-10-10 02:15:21","86.7","4.5","91.2","pass 143ms","pass 31ms","pass 11.26s","0"
"rc7f","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1162","True","False","2026-10-10 02:16:36","2026-10-10 02:17:38","2026-10-10 02:20:32","1.0","2.9","3.9","pass 302ms","FAIL 553ms 0x100000","pass 11.26s","1"
"rc7f","fdd7a8b","OSFile22","Core","osmatrix\NtfsLiveSubject","1162","True","False","2026-10-10 02:16:36","2026-10-10 02:17:38","2026-10-10 02:22:01","1.0","4.4","5.4","pass 174ms","FAIL 80ms 0x100000","pass 11.72s","1"
"rc7g","fdd7a8b","OSFile25","Desktop","osmatrix\NtfsLiveSubject","1171","True","False","2026-10-10 02:23:08","2026-10-10 02:28:21","2026-10-10 02:31:16","5.2","2.9","8.1","pass 265ms","pass 35ms","pass 11.65s","0"
"rc7g","fdd7a8b","OSFile25","Core","osmatrix\NtfsLiveSubject","1171","True","False","2026-10-10 02:23:08","2026-10-10 02:28:21","2026-10-10 02:34:21","5.2","6.0","11.2","pass 161ms","pass 27ms","pass 12.04s","0"
"rc7h","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1180","True","False","2026-10-10 02:35:34","2026-10-10 02:36:51","2026-10-10 02:39:41","1.3","2.8","4.1","pass 292ms","FAIL 626ms 0x100000","pass 11.23s","1"
"rc7h","fdd7a8b","OSFile22","Core","osmatrix\NtfsLiveSubject","1180","True","False","2026-10-10 02:35:34","2026-10-10 02:36:51","2026-10-10 02:41:11","1.3","4.3","5.6","pass 174ms","FAIL 80ms 0x100000","pass 11.3s","1"
"rc7i","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1189","True","False","2026-10-10 02:42:17","2026-10-10 02:46:06","2026-10-10 02:49:01","3.8","2.9","6.7","FAIL 827ms 0x100000","pass 40ms","pass 11.02s","1"
"rc7i","fdd7a8b","OSFile22","Core","osmatrix\NtfsLiveSubject","1189","True","False","2026-10-10 02:42:17","2026-10-10 02:46:06","2026-10-10 02:50:32","3.8","4.4","8.3","pass 169ms","pass 34ms","pass 12.05s","0"
"rc7j","962887a","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1198","True","False","2026-10-10 02:51:38","2026-10-10 02:52:37","2026-10-10 02:55:28","1.0","2.9","3.8","FAIL 884ms 0x100000","FAIL 36ms 0x100000","pass 11s","2"
"rc7j","962887a","OSFile22","Core","osmatrix\NtfsLiveSubject","1198","True","False","2026-10-10 02:51:38","2026-10-10 02:52:37","2026-10-10 02:56:55","1.0","4.3","5.3","FAIL 226ms 0x100000","FAIL 34ms 0x100000","pass 11.46s","2"
"rc7k","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1207","True","False","2026-10-10 02:58:00","2026-10-10 02:58:58","2026-10-10 03:01:50","1.0","2.9","3.8","pass 312ms","pass 36ms","pass 12.26s","0"
"rc7k","83149ee","OSFile22","Core","osmatrix\NtfsLiveSubject","1207","True","False","2026-10-10 02:58:00","2026-10-10 02:58:58","2026-10-10 03:03:20","1.0","4.4","5.3","pass 163ms","pass 25ms","pass 12.1s","0"
"rc7l","fdd7a8b","OSFile19","Desktop","osmatrix\NtfsLiveSubject0602","1279","False","False","2026-10-10 03:04:28","2026-10-10 03:45:30","2026-10-10 03:48:27","41.0","3.0","44.0","pass 300ms","pass 52ms","pass 11.59s","0"
"rc7l","fdd7a8b","OSFile19","Core","osmatrix\NtfsLiveSubject0602","1279","False","False","2026-10-10 03:04:28","2026-10-10 03:45:30","2026-10-10 03:49:59","41.0","4.5","45.5","pass 148ms","pass 56ms","pass 11.03s","0"
"rc7l","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject6688","1290","False","False","2026-10-10 03:51:09","2026-10-10 03:52:12","2026-10-10 03:55:03","1.1","2.9","3.9","pass 298ms","pass 42ms","pass 11.35s","0"
"rc7l","fdd7a8b","OSFile22","Core","osmatrix\NtfsLiveSubject6688","1290","False","False","2026-10-10 03:51:09","2026-10-10 03:52:12","2026-10-10 03:56:29","1.1","4.3","5.3","pass 141ms","pass 34ms","pass 11.24s","0"
"rc7l","fdd7a8b","OSFile25","Desktop","osmatrix\NtfsLiveSubject4884","1298","False","False","2026-10-10 03:57:33","2026-10-10 03:58:35","2026-10-10 04:01:28","1.0","2.9","3.9","pass 303ms","pass 46ms","pass 11.29s","0"
"rc7l","fdd7a8b","OSFile25","Core","osmatrix\NtfsLiveSubject4884","1298","False","False","2026-10-10 03:57:33","2026-10-10 03:58:35","2026-10-10 04:04:32","1.0","6.0","7.0","pass 152ms","pass 27ms","pass 12.12s","0"
"ab0","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1318","False","False","2026-10-10 04:06:55","2026-10-10 04:58:03","2026-10-10 05:00:57","51.1","2.9","54.0","pass 289ms","pass 42ms","pass 11.51s","0"
"ab1","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1326","True","False","2026-10-10 05:02:19","2026-10-10 05:03:18","2026-10-10 05:06:09","1.0","2.9","3.8","FAIL 817ms 0x100000","FAIL 34ms 0x100000","pass 11.17s","2"
"ab2","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1334","True","False","2026-10-10 05:07:28","2026-10-10 05:08:28","2026-10-10 05:11:12","1.0","2.7","3.7","pass 297ms","pass 55ms","pass 12.2s","0"
"ab3","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1342","True","False","2026-10-10 05:12:32","2026-10-10 05:13:31","2026-10-10 05:16:23","1.0","2.9","3.9","FAIL 825ms 0x100000","FAIL 36ms 0x100000","pass 12.18s","2"
"ab4","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1350","True","False","2026-10-10 05:17:43","2026-10-10 05:18:42","2026-10-10 05:21:34","1.0","2.9","3.9","pass 283ms","pass 47ms","pass 12.25s","0"
"ab5","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1358","True","False","2026-10-10 05:23:04","2026-10-10 05:24:05","2026-10-10 05:26:57","1.0","2.9","3.9","FAIL 804ms 0x100000","FAIL 35ms 0x100000","pass 11.71s","2"
"ab6","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject","1366","True","False","2026-10-10 05:28:16","2026-10-10 05:29:14","2026-10-10 05:32:04","1.0","2.8","3.8","pass 289ms","pass 51ms","pass 11.13s","0"
"ab7","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject8013","1374","False","False","2026-10-10 05:33:22","2026-10-10 05:37:15","2026-10-10 05:40:11","3.9","2.9","6.8","pass 280ms","pass 45ms","pass 12.2s","0"
"ab8","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject0900","1382","False","False","2026-10-10 05:41:34","2026-10-10 05:42:38","2026-10-10 05:45:32","1.1","2.9","4.0","pass 300ms","pass 40ms","pass 11.89s","0"
"ab9","83149ee","OSFile22","Desktop","osmatrix\NtfsLiveSubject7705","1391","False","False","2026-10-10 05:46:54","2026-10-10 05:48:00","2026-10-10 05:50:54","1.1","2.9","4.0","pass 265ms","pass 40ms","pass 12.16s","0"
"ab10","fdd7a8b","OSFile22","Desktop","osmatrix\NtfsLiveSubject8799","1399","False","False","2026-10-10 05:52:14","2026-10-10 05:53:18","2026-10-10 05:56:12","1.1","2.9","4.0","pass 272ms","pass 35ms","pass 11.07s","0"
1 Run Candidate FileServer Edition Subject SubjectRid SameNameAsPreviousCell SameAccountAsPreviousCell PreviousRemoval AccountsCreated AdminRoleStarted MinutesRemovalToCreation MinutesCreationToAdmin MinutesRemovalToAdmin T1ServerNameFileServer T2DefaultServerName T3UnreachableServerName EffectiveAccessFailures
2 rc7c 83149ee OSFile19 Desktop osmatrix\NtfsLiveSubject 1110 False False 2026-10-09 23:43:43 2026-10-09 23:46:33 2.8 pass 276ms pass 43ms pass 12.1s 0
3 rc7c 83149ee OSFile19 Core osmatrix\NtfsLiveSubject 1110 False False 2026-10-09 23:43:43 2026-10-09 23:48:09 4.4 pass 165ms pass 24ms pass 11.06s 0
4 rc7c 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1110 True True 2026-10-09 23:49:38 2026-10-09 23:52:16 2.6 pass 439ms pass 37ms pass 11.47s 0
5 rc7c 83149ee OSFile22 Core osmatrix\NtfsLiveSubject 1110 True True 2026-10-09 23:49:38 2026-10-09 23:53:48 4.2 pass 164ms pass 33ms pass 11.76s 0
6 rc7c 83149ee OSFile25 Desktop osmatrix\NtfsLiveSubject 1110 True True 2026-10-09 23:55:37 2026-10-09 23:58:32 2.9 pass 423ms pass 41ms pass 11.95s 0
7 rc7c 83149ee OSFile25 Core osmatrix\NtfsLiveSubject 1110 True True 2026-10-09 23:55:37 2026-10-10 00:01:33 5.9 pass 129ms pass 40ms pass 11.58s 0
8 rc7e 83149ee OSFile19 Desktop osmatrix\NtfsLiveSubject 1130 True False 2026-10-10 00:16:22 2026-10-10 00:20:59 2026-10-10 00:23:36 4.6 2.6 7.2 pass 250ms pass 37ms pass 11.55s 0
9 rc7e 83149ee OSFile19 Core osmatrix\NtfsLiveSubject 1130 True False 2026-10-10 00:16:22 2026-10-10 00:20:59 2026-10-10 00:25:03 4.6 4.1 8.7 pass 147ms pass 26ms pass 11.16s 0
10 rc7e 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1130 True True 2026-10-10 00:26:33 2026-10-10 00:29:19 2.8 pass 445ms pass 36ms pass 12.11s 0
11 rc7e 83149ee OSFile22 Core osmatrix\NtfsLiveSubject 1130 True True 2026-10-10 00:26:33 2026-10-10 00:30:47 4.2 pass 158ms pass 31ms pass 12.09s 0
12 rc7e 83149ee OSFile25 Desktop osmatrix\NtfsLiveSubject 1141 True False 2026-10-10 00:31:54 2026-10-10 00:32:55 2026-10-10 00:35:48 1.0 2.9 3.9 pass 256ms pass 28ms pass 12.11s 0
13 rc7e 83149ee OSFile25 Core osmatrix\NtfsLiveSubject 1141 True False 2026-10-10 00:31:54 2026-10-10 00:32:55 2026-10-10 00:41:22 1.0 8.5 9.5 pass 137ms pass 50ms pass 11.22s 0
14 rc7f fdd7a8b OSFile19 Desktop osmatrix\NtfsLiveSubject 1153 True False 2026-10-10 00:44:09 2026-10-10 02:10:50 2026-10-10 02:13:46 86.7 2.9 89.6 pass 287ms pass 44ms pass 11.53s 0
15 rc7f fdd7a8b OSFile19 Core osmatrix\NtfsLiveSubject 1153 True False 2026-10-10 00:44:09 2026-10-10 02:10:50 2026-10-10 02:15:21 86.7 4.5 91.2 pass 143ms pass 31ms pass 11.26s 0
16 rc7f fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1162 True False 2026-10-10 02:16:36 2026-10-10 02:17:38 2026-10-10 02:20:32 1.0 2.9 3.9 pass 302ms FAIL 553ms 0x100000 pass 11.26s 1
17 rc7f fdd7a8b OSFile22 Core osmatrix\NtfsLiveSubject 1162 True False 2026-10-10 02:16:36 2026-10-10 02:17:38 2026-10-10 02:22:01 1.0 4.4 5.4 pass 174ms FAIL 80ms 0x100000 pass 11.72s 1
18 rc7g fdd7a8b OSFile25 Desktop osmatrix\NtfsLiveSubject 1171 True False 2026-10-10 02:23:08 2026-10-10 02:28:21 2026-10-10 02:31:16 5.2 2.9 8.1 pass 265ms pass 35ms pass 11.65s 0
19 rc7g fdd7a8b OSFile25 Core osmatrix\NtfsLiveSubject 1171 True False 2026-10-10 02:23:08 2026-10-10 02:28:21 2026-10-10 02:34:21 5.2 6.0 11.2 pass 161ms pass 27ms pass 12.04s 0
20 rc7h fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1180 True False 2026-10-10 02:35:34 2026-10-10 02:36:51 2026-10-10 02:39:41 1.3 2.8 4.1 pass 292ms FAIL 626ms 0x100000 pass 11.23s 1
21 rc7h fdd7a8b OSFile22 Core osmatrix\NtfsLiveSubject 1180 True False 2026-10-10 02:35:34 2026-10-10 02:36:51 2026-10-10 02:41:11 1.3 4.3 5.6 pass 174ms FAIL 80ms 0x100000 pass 11.3s 1
22 rc7i fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1189 True False 2026-10-10 02:42:17 2026-10-10 02:46:06 2026-10-10 02:49:01 3.8 2.9 6.7 FAIL 827ms 0x100000 pass 40ms pass 11.02s 1
23 rc7i fdd7a8b OSFile22 Core osmatrix\NtfsLiveSubject 1189 True False 2026-10-10 02:42:17 2026-10-10 02:46:06 2026-10-10 02:50:32 3.8 4.4 8.3 pass 169ms pass 34ms pass 12.05s 0
24 rc7j 962887a OSFile22 Desktop osmatrix\NtfsLiveSubject 1198 True False 2026-10-10 02:51:38 2026-10-10 02:52:37 2026-10-10 02:55:28 1.0 2.9 3.8 FAIL 884ms 0x100000 FAIL 36ms 0x100000 pass 11s 2
25 rc7j 962887a OSFile22 Core osmatrix\NtfsLiveSubject 1198 True False 2026-10-10 02:51:38 2026-10-10 02:52:37 2026-10-10 02:56:55 1.0 4.3 5.3 FAIL 226ms 0x100000 FAIL 34ms 0x100000 pass 11.46s 2
26 rc7k 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1207 True False 2026-10-10 02:58:00 2026-10-10 02:58:58 2026-10-10 03:01:50 1.0 2.9 3.8 pass 312ms pass 36ms pass 12.26s 0
27 rc7k 83149ee OSFile22 Core osmatrix\NtfsLiveSubject 1207 True False 2026-10-10 02:58:00 2026-10-10 02:58:58 2026-10-10 03:03:20 1.0 4.4 5.3 pass 163ms pass 25ms pass 12.1s 0
28 rc7l fdd7a8b OSFile19 Desktop osmatrix\NtfsLiveSubject0602 1279 False False 2026-10-10 03:04:28 2026-10-10 03:45:30 2026-10-10 03:48:27 41.0 3.0 44.0 pass 300ms pass 52ms pass 11.59s 0
29 rc7l fdd7a8b OSFile19 Core osmatrix\NtfsLiveSubject0602 1279 False False 2026-10-10 03:04:28 2026-10-10 03:45:30 2026-10-10 03:49:59 41.0 4.5 45.5 pass 148ms pass 56ms pass 11.03s 0
30 rc7l fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject6688 1290 False False 2026-10-10 03:51:09 2026-10-10 03:52:12 2026-10-10 03:55:03 1.1 2.9 3.9 pass 298ms pass 42ms pass 11.35s 0
31 rc7l fdd7a8b OSFile22 Core osmatrix\NtfsLiveSubject6688 1290 False False 2026-10-10 03:51:09 2026-10-10 03:52:12 2026-10-10 03:56:29 1.1 4.3 5.3 pass 141ms pass 34ms pass 11.24s 0
32 rc7l fdd7a8b OSFile25 Desktop osmatrix\NtfsLiveSubject4884 1298 False False 2026-10-10 03:57:33 2026-10-10 03:58:35 2026-10-10 04:01:28 1.0 2.9 3.9 pass 303ms pass 46ms pass 11.29s 0
33 rc7l fdd7a8b OSFile25 Core osmatrix\NtfsLiveSubject4884 1298 False False 2026-10-10 03:57:33 2026-10-10 03:58:35 2026-10-10 04:04:32 1.0 6.0 7.0 pass 152ms pass 27ms pass 12.12s 0
34 ab0 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1318 False False 2026-10-10 04:06:55 2026-10-10 04:58:03 2026-10-10 05:00:57 51.1 2.9 54.0 pass 289ms pass 42ms pass 11.51s 0
35 ab1 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1326 True False 2026-10-10 05:02:19 2026-10-10 05:03:18 2026-10-10 05:06:09 1.0 2.9 3.8 FAIL 817ms 0x100000 FAIL 34ms 0x100000 pass 11.17s 2
36 ab2 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1334 True False 2026-10-10 05:07:28 2026-10-10 05:08:28 2026-10-10 05:11:12 1.0 2.7 3.7 pass 297ms pass 55ms pass 12.2s 0
37 ab3 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1342 True False 2026-10-10 05:12:32 2026-10-10 05:13:31 2026-10-10 05:16:23 1.0 2.9 3.9 FAIL 825ms 0x100000 FAIL 36ms 0x100000 pass 12.18s 2
38 ab4 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1350 True False 2026-10-10 05:17:43 2026-10-10 05:18:42 2026-10-10 05:21:34 1.0 2.9 3.9 pass 283ms pass 47ms pass 12.25s 0
39 ab5 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject 1358 True False 2026-10-10 05:23:04 2026-10-10 05:24:05 2026-10-10 05:26:57 1.0 2.9 3.9 FAIL 804ms 0x100000 FAIL 35ms 0x100000 pass 11.71s 2
40 ab6 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject 1366 True False 2026-10-10 05:28:16 2026-10-10 05:29:14 2026-10-10 05:32:04 1.0 2.8 3.8 pass 289ms pass 51ms pass 11.13s 0
41 ab7 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject8013 1374 False False 2026-10-10 05:33:22 2026-10-10 05:37:15 2026-10-10 05:40:11 3.9 2.9 6.8 pass 280ms pass 45ms pass 12.2s 0
42 ab8 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject0900 1382 False False 2026-10-10 05:41:34 2026-10-10 05:42:38 2026-10-10 05:45:32 1.1 2.9 4.0 pass 300ms pass 40ms pass 11.89s 0
43 ab9 83149ee OSFile22 Desktop osmatrix\NtfsLiveSubject7705 1391 False False 2026-10-10 05:46:54 2026-10-10 05:48:00 2026-10-10 05:50:54 1.1 2.9 4.0 pass 265ms pass 40ms pass 12.16s 0
44 ab10 fdd7a8b OSFile22 Desktop osmatrix\NtfsLiveSubject8799 1399 False False 2026-10-10 05:52:14 2026-10-10 05:53:18 2026-10-10 05:56:12 1.1 2.9 4.0 pass 272ms pass 35ms pass 11.07s 0

760
Tests/Lab/Acceptance-2026-10-10-os-matrix.md

@ -0,0 +1,760 @@
# Operating-system matrix acceptance, 2026-10-10
Live and local acceptance of NTFSSecurity candidates on more operating systems
than the first lab has (Decision 24, handoff 2 of the 5.0.0 quality gate):
Windows Server 2019, 2022, and 2025 as file servers and Windows 11 as client, in
Windows PowerShell 5.1 and PowerShell 7, elevated and as a basic user. The
candidates are local builds of `ai/quality-gate-lab-matrix`, tested from their
extracted packages with `-ModulePath`. None is a published package, so this
record isn't the acceptance of a release (see the limits at the end), and it
isn't a claim that the quality gate is complete.
## Result
- The module's own suite, 1,011 cases per configuration (1,010 on the host), ran
on five operating systems and on the host in four configurations each. The
final candidate (`fdd7a8b`) has no failure in any of the 24 runs; every
skipped test is also skipped on the host.
- The live controller ran for the final candidate in three cells of the matrix
(the Windows 11 client with each file server, both editions, every role) in
one sequence, after the fixture got a new account name for each new fixture:
1,374 passed, 0 failed, 12 skipped (case 9 and the module test of the Server
role). An earlier run of the same cells with the old controller had failed in
the Windows Server 2022 cell. A replay showed that the baseline fails the same
way there, so the module doesn't decide the outcome: the failures depend on the
position of the cell (six replay runs can't rule out a small effect of the
module; see "The effective-access failures of the Admin role").
- The final candidate also passed the live controller in the first lab, where
case 9 runs: 245 passed, 0 failed, 1 skipped in each edition (see "First lab,
final candidate (case 9)").
- The matrix found three defects of the module, fixed in two commits on the
branch, and each was red on the machines where it shows before its fix and
green after it: `Get-NTFSInheritance -SecurityDescriptor` for an item without
audit entries and `Get-NTFSEffectiveAccess -ServerName ''` (`962887a`, two
fixes), and `Get-NTFSEffectiveAccess` for a user who isn't an administrator on
a computer in a domain (`fdd7a8b`). The first two showed on Windows Server 2022
and 2025 and on Windows 11 26H1, the third on every machine of the domain.
- The controller had four defects of its own: three in cleanup and setup
(`7d47316`) and the reuse of the name of the account of case 3 (`1dec389`).
Cells that followed each other failed in the effective-access tests of the
Admin role when the account of case 3 was deleted and created again under the
same name: the remote authorization managers of the client and of the file
server returned no groups for the new account, for the baseline and for the
final candidate alike. A model with a lifetime of about ten minutes fits every run; the mechanism
in Windows isn't known. This looked like a regression of the module until the
baseline failed the same way in a replay of the same cells.
- Windows 11 26H1 (10.0.28000) can't keep a secure channel to the Windows
Server 2025 domain controller of this lab, so it runs the module's suite only.
The domain client is Windows 11 Enterprise Evaluation 22H2.
- Open: the published package in every cell and in the first lab (stage D of the
gate), and the maintainer's decisions listed at the end. The matrix lab has no
trusts, so case 9 runs only in the first lab.
## Machines
All machines are virtual machines on the Hyper-V host in the lab
`NtfsSecurityOsMatrixLab` (domain `osmatrix.net`, switch `192.168.12.0/24`),
which AutomatedLab deployed beside the other labs without touching them. The
.NET Framework release number is the one that the readiness check read.
| Machine | Role | Operating system | Build | .NET Framework | Windows PowerShell | PowerShell 7 |
| --- | --- | --- | --- | ---: | --- | --- |
| OSDC1 | Root domain controller | Windows Server 2025 Datacenter | 10.0.26100.32690 | 533509 | 5.1.26100.32684 | 7.6.3 |
| OSFile19 | File server | Windows Server 2019 Datacenter | 10.0.17763.1217 | 461814 | 5.1.17763.1007 | 7.6.3 |
| OSFile22 | File server | Windows Server 2022 Datacenter | 10.0.20348.4773 | 528449 | 5.1.20348.4294 | 7.6.3 |
| OSFile25 | File server | Windows Server 2025 Datacenter | 10.0.26100.32690 | 533509 | 5.1.26100.32684 | 7.6.3 |
| OSWin11E | Client | Windows 11 Enterprise Evaluation 22H2 | 10.0.22621.525 | 533320 | 5.1.22621.169 | 7.6.3 |
| OSWin11 | Suite only | Windows 11 Pro 26H1 | 10.0.28000.1836 | 533510 | 5.1.28000.1830 | 7.6.3 |
| Host | Reference for the suite | Windows Server 2025 Datacenter | 10.0.26100.33438 | 533509 | 5.1.26100.33438 | 7.6.6 |
Windows 11 reports `Windows 10` as the product name in the registry; the builds
are Windows 11. The VMs have no internet, so PowerShell 7.6.3 and Pester 5.7.1
came from the host. Every machine passed a readiness check before a cell: WinRM
with the lab account, LDAP, Kerberos, the secure channel, the clocks, the tools,
and the Pester version.
## Candidates and artifact identity
Each candidate is a Release build (.NET Framework 4.5.2) in an isolated worktree
of its commit, packaged by `.github/scripts/New-ModulePackage.ps1`. All 11 files
of every tested module folder equal the extracted `NTFSSecurity.zip` byte for
byte. The builds aren't byte-reproducible: `PrivilegeControl.dll` and
`ProcessPrivileges.dll` differ between the candidates although no source of
theirs changed, so the hashes belong to one build each.
| | Baseline `83149ee` | Candidate `962887a` | Final `fdd7a8b` |
| --- | --- | --- | --- |
| What it is | Head of #118 | Two fixes in the module | Three fixes in the module |
| `NTFSSecurity.dll` | `40D0C8A6B819F15A…` | `9AC1169F91687495…` | `B0631389E68C8244…` |
| `Security2.dll` | `804D199335CA0D6A…` | `FF314FACCF01676A…` | `A4D744579E8FF3BE…` |
| `NTFSSecurity.5.0.0-rc7.nupkg` | `2AAE3403A2D1C3AE…` | `ACFA247BCD5AD33D…` | `1A112B8CBBE27F9D…` |
| `NTFSSecurity.zip` | `A5AFA241DCA5DF87…` | `CD836E39C6B22EB3…` | `3DF48E5C590A9E38…` |
The full SHA-256 values and the hashes of every result file are in the local
evidence (see the end). The tests of the suite are the files of the working
tree at the commit of the run. The live tests (Git blob
`efe36e5073b9b10742ca7de242ddcbe90d8eda62`) are those of `fdd7a8b` in every run
from `rc7f` to `rc7l` and in the first-lab run `fl1`; the replay `ab0` to `ab10`
ran the same file with one diagnostic test added (blob
`72c12fe09e4005e048a8aed0fa02b9a922c58f34`, see "The effective-access failures
of the Admin role"). The controller of the cells `rc7f` to `rc7k` and of the
replay cells `ab0` to `ab6` is the blob `683aee91ec8805d77a33b2d368acaf876724fa32`
(`fdd7a8b`). The cells of `rc7l` and the replay cells `ab7` to `ab10` ran with the
blob `9917cac5820ed20ed2eb5592eff06894677f9874` (`1dec389`), and the first-lab run
`fl1` with the blob `d269e0fe6ba5f72894dd2dcba5dca7f2dc56624f` (the head of the
branch then, which differs from `1dec389` by a comment). The earlier cells of the
baseline ran with the controller blobs `0b46427b…` and `d485b1d0…` (`rc7d` with
`4cac0d0b…`) and the live tests `67b85efe…`, before the cleanup fixes.
## Method
- **Module's own suite.** `Acceptance\Run-MatrixLocalSuite.ps1` copies the 18
behavior test files and the candidate to a machine and runs them there in a
new Windows PowerShell process and a new PowerShell 7 process, elevated and as
a basic user. The basic user is the token that
`.github\scripts\Invoke-TestsAsBasicUser.ps1` makes (SAFER level Normal User),
so the tests that need a missing privilege skip in the elevated mode and run
in this one. The processes run as scheduled tasks with a batch logon at the
highest run level: a process that starts from a remoting session has every
privilege enabled and no credentials of its own, which eight tests don't
expect. The host runs the same stage as the reference. A skipped test counts
as a difference when only one side skips it; the skipped lists are compared as
multisets of test names.
- **Live controller.** `Acceptance\Run-MatrixSequence.ps1` runs, for each file
server with the client `OSWin11E`: the readiness of every machine, the
unmodified controller of the repository in both editions, the validation of
every role from the result files (`Validate-LabResults.ps1`, never from the
marker `DONE`), a snapshot of the fixture SIDs, the removal of the fixture,
and an independent check of the end state (`Test-MatrixCleanup.ps1`).
- **Probe.** `Acceptance\Probe-EffectiveAccess.ps1` asks
`Get-NTFSEffectiveAccess` the same questions under four tokens on one machine
and writes the result and the failing call of each: the elevated lab account,
the SAFER token of a basic user, a local standard user, and a standard user of
the domain. It creates the two standard users with passwords that exist only
in memory and removes them, their profiles, and their group membership again.
- **Account probe.** `Acceptance\Probe-AccountRecreation.ps1` deletes an account
and creates it again with the same name in a loop. It shows the token that
Kerberos S4U logons give on the domain controller, the client, and the file
server, and what `Get-NTFSEffectiveAccess` of each module under test returns
from the client (see "The effective-access failures of the Admin role"). Its
accounts, folder, and files are named `NtfsProbe*`, which `Test-MatrixCleanup.ps1`
reports if they stay.
- **Replay.** The cells that failed were run again back to back, one edition, one
file server, with the baseline and the final candidate alternating: after a
restart of the client, one `Acceptance\Run-MatrixSequence.ps1 -Edition Desktop
-FileServer OSFile22` per cell with a different `-ModulePath`, from frozen
copies of the kit and the controller so that no edit could change a run in
progress. The live tests of the replay had one test added that is not in the
repository and prints the state of the subject account after the three
effective-access tests. The kit has the tools that read the
result: `Acceptance\Export-CellTimeline.ps1` (the timeline of the Admin role of
every cell and edition: the module, the account, the times, and the three
tests) and
`Acceptance\Test-StaleAuthzModel.ps1` (the model of the failures, replayed
against that timeline).
## Results
### Live controller
The final candidate (`fdd7a8b`) in the three cells of the matrix, with the
Windows 11 client `OSWin11E`, the controller of `1dec389` (Git blob
`9917cac5820ed20ed2eb5592eff06894677f9874`) and the live tests of blob
`efe36e5073b9b10742ca7de242ddcbe90d8eda62`: run `rc7l`, one sequence, 03:45 to
04:07 UTC. Every role was checked from the result files
(`Validate-LabResults.ps1` printed `LIVE_RESULT_VERIFIED`). Passed / failed /
skipped:
| File server | Edition | Delegate | ServerAdmin | Admin | Server |
| --- | --- | --- | --- | --- | --- |
| OSFile19 (Windows Server 2019) | Windows PowerShell | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
| OSFile19 (Windows Server 2019) | PowerShell 7 | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
| OSFile22 (Windows Server 2022) | Windows PowerShell | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
| OSFile22 (Windows Server 2022) | PowerShell 7 | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
| OSFile25 (Windows Server 2025) | Windows PowerShell | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
| OSFile25 (Windows Server 2025) | PowerShell 7 | 69 / 0 / 0 | 36 / 0 / 0 | 51 / 0 / 1 | 73 / 0 / 1 |
That is 1,374 passed, 0 failed, and 12 skipped for the three cells. The skipped
tests are the same in every cell: case 9 (the Admin role skips "Get-NTFSOrphanedAccess
should not report the entries of the accounts" of other domains and forests,
because the matrix lab has no foreign domain) and the Server role's test of the
module version (that role runs without the module). Each cell had a new
fixture: the subject of case 3 was `NtfsLiveSubject0602` in the cell of
OSFile19 and `NtfsLiveSubject6688` in the cell of OSFile22.
All runs of the controller, as the table of cells
([Cells.csv](Acceptance-2026-10-10-os-matrix-Cells.csv)) lists them. The runs
before `rc7l` used the controller with the fixed name of the subject:
| Run | Candidate | Cells | Result per edition and cell |
| --- | --- | --- | --- |
| `rc7c`, `rc7e` | Baseline `83149ee`, tests before the new cases | OSFile19, 22, 25 | 227 passed, 0 failed, 2 skipped in every cell. In `rc7c`, the failed cleanup of the first cell left the accounts in place through all three cells. `rc7d` between them created accounts and removed them 36 seconds later, because its setup failed ("Failed to compare two elements in the array", fixed in `7d47316`) before any test ran. In `rc7e`, the first cell created new accounts 4.6 minutes after that removal, the second reused them, and the third created new accounts 1.0 minute after the previous removal |
| `rc7f` | Final `fdd7a8b` | OSFile19, OSFile22 | OSFile19: 229 / 0 / 2. OSFile22: 228 / 1 / 2, the effective-access test of the Admin role |
| `rc7g` | Final | OSFile25 | 229 / 0 / 2 |
| `rc7h` | Final | OSFile22 | 228 / 1 / 2, the same test |
| `rc7i` | Final | OSFile22 | Windows PowerShell 228 / 1 / 2 (Admin), PowerShell 7 229 / 0 / 2 |
| `rc7j` | `962887a` (without the third fix) | OSFile22 | 225 / 4 / 2: the two new tests of the ServerAdmin role (red without the fix, "Access is denied" for `localhost` and for the name of the client) and two tests of the Admin role |
| `rc7k` | Baseline `83149ee`, with the final tests | OSFile22 | 227 / 2 / 2: the two new tests of the ServerAdmin role; the Admin role passed |
The failures of the Admin role in `rc7f`, `rc7h`, `rc7i`, and `rc7j` don't depend
on the module: the baseline fails the same way in a replay of the cells (see "The
effective-access failures of the Admin role"). The two
failures of the ServerAdmin role in `rc7j` and `rc7k` are the red state of the
new live tests, as intended; they pass in `rc7f`, `rc7g`, `rc7h`, `rc7i`, and
`rc7l`. The end-state check after each cell of `rc7l` found the fixture gone
(no organizational unit, account, share, folder, local group, membership, or
profile) and reported only the staging folders of the earlier suite runs, which
`Test-MatrixCleanup.ps1` didn't check before (see the limits).
### First lab, final candidate (case 9)
Case 9, the accounts of other domains and forests, needs the trusts of the
first lab, so the cells of the matrix skip it. The final candidate (`fdd7a8b`,
from the same extracted module folder as in the matrix) ran through the
controller of the repository (blob `d269e0fe6ba5f72894dd2dcba5dca7f2dc56624f`) in
`WindowsAccessControlLab`, both editions, on 2026-10-10 from 06:14 to 06:31 UTC
(run `fl1`): the domain controller `F1ADC1`, the file server `F1AFile2`, and the
client `F1AFile1` (all Windows Server 2025 Datacenter 10.0.26100.32690, domain
`a.forest1.net`), with the foreign domain controllers `F1BDC1`, `F2DC1`, and
`F3DC1`. `Validate-LabResults.ps1` printed `LIVE_RESULT_VERIFIED`. Passed /
failed / skipped, the same in both editions
([FirstLab.csv](Acceptance-2026-10-10-os-matrix-FirstLab.csv)):
| Role | Passed / failed / skipped |
| --- | --- |
| Delegate | 69 / 0 / 0 |
| ServerAdmin | 36 / 0 / 0 |
| Admin | 64 / 0 / 0 |
| Server | 76 / 0 / 1 |
That is 245 passed, 0 failed, and 1 skipped per edition; the skipped test is the
test of the module version in the Server role, which runs without the module. A
cell of the matrix has 229 passed and 2 skipped. The 16 tests more that passed
here are the tests of case 9: 15 that a matrix cell doesn't have (12 in the
Admin role and 3 in the Server role: the entries of `NtfsLiveForeign` of the
three foreign domains by `Get-NTFSAccess`, `Add-NTFSAccess`, `Remove-NTFSAccess`,
and `Get-NTFSEffectiveAccess`, and on the file server), and the test of
`Get-NTFSOrphanedAccess` that the matrix cells skip. Every test of a matrix
cell is in this run too (a comparison of the test names of `rc7l` OSFile25 and
this run found none that only the cell has). The fixture was removed with
`-RemoveFixture`, and the independent check (`Test-MatrixCleanup.ps1`, with the
10 SIDs that it recorded before: the accounts of the lab domain and
`NtfsLiveForeign` in each of the three foreign domains) found the four domains
and both machines clean: no organizational unit, account, share, folder, local
group, membership, or profile of the fixture, and none of the residue that the
check counted then (scheduled tasks, stage items, probe users); its verdict was
CLEAN. The check has counted the profiles and the log-group entries of the
account probe since the review that followed, and a `Verify` with that version
(07:29 UTC, the same SIDs) found none on the two machines either. This
is one run of one candidate. The baseline didn't run in the first lab on this
occasion, so the record says nothing about the red state of the new tests there.
### The module's own suite, final candidate
Passed / failed / skipped. Every configuration has 1,011 cases on the machines
of the domain and 1,010 on the host, which has no DNS domain and so doesn't run
the case for the fully qualified name of its computer.
| Machine | Elevated, Windows PowerShell | Elevated, PowerShell 7 | Basic user, Windows PowerShell | Basic user, PowerShell 7 |
| --- | --- | --- | --- | --- |
| OSFile19 (Server 2019) | 994 / 0 / 17 | 992 / 0 / 19 | 782 / 0 / 229 | 780 / 0 / 231 |
| OSFile22 (Server 2022) | 994 / 0 / 17 | 992 / 0 / 19 | 782 / 0 / 229 | 780 / 0 / 231 |
| OSFile25 (Server 2025) | 994 / 0 / 17 | 992 / 0 / 19 | 782 / 0 / 229 | 780 / 0 / 231 |
| OSWin11E (Windows 11 22H2, the client of the cells) | 994 / 0 / 17 | 992 / 0 / 19 | 782 / 0 / 229 | 780 / 0 / 231 |
| OSWin11 (Windows 11 26H1) | 994 / 0 / 17 | 992 / 0 / 19 | 782 / 0 / 229 | 780 / 0 / 231 |
| Host (reference) | 993 / 0 / 17 | 991 / 0 / 19 | 781 / 0 / 229 | 779 / 0 / 231 |
On every machine, the skipped tests are the same as on the host, name for name,
in every configuration. They are tests that need the other privilege level or
the other edition and that run in another configuration, as the earlier
analysis of the skipped rows found (all 578 skipped rows of the suite at
`5a5d58b` ran in two other configurations). The two disabled-audit-inheritance
tests that `962887a` added are skipped without the Security privilege, so the
basic configurations skip two cases more than before (229 instead of 227).
### What the fixes changed
The same tests, the same machines, and three builds. The baseline and the
candidate `962887a` were run with the tests of the final commit, so the guards
of the later fixes were present and red.
| Candidate | Elevated | Basic user |
| --- | --- | --- |
| Baseline `83149ee` (Server 2022 and 2025) | 4 failures in every configuration | 20 failures in every configuration |
| `962887a` (all five machines of the domain) | 0 failures | 20 failures in every configuration |
| Final `fdd7a8b` (all five machines of the domain) | 0 failures | 0 failures |
The four elevated failures of the baseline are the same on Server 2022 and
2025, in both editions:
1. `Get-NTFSInheritance` with a security descriptor, "Should report the same
state as for the path of the item" (existing test),
2. the same for "a file without audit entries" and
3. "a folder without audit entries" (two new guards),
4. `Get-NTFSEffectiveAccess`, "Should return the result of this computer and
warn for an empty -ServerName" (existing test).
The 20 failures in the basic-user mode are the same set on every machine of the
domain and in both editions, once the name of the computer in two test names is
set aside. All of them call `Get-NTFSEffectiveAccess` without a reachable
remote authorization manager: 11 in its own tests, 5 in the tests of a later
command that ends the pipeline, 2 for an unresolved identity, 1 for a path that
doesn't exist, and 1 for an item whose owner may not read its permissions. Each
fails with "Could not get effective permissions from machine 'localhost'. The
error is 'Access is denied'". The host and the CI runners aren't in a domain and
passed them all along. The failing names of every run are in
[the failures table](Acceptance-2026-10-10-os-matrix-Failures.csv).
## Defects found
### In the module
1. **`Get-NTFSInheritance -SecurityDescriptor` for an item without audit
entries** reported the audit inheritance as disabled, where `-Path` reported
it as enabled. On Windows Server 2022 and 2025 and on Windows 11 26H1, .NET
reports the SACL of such an item as protected from inheritance when it reads
all sections together, and as not protected when it reads the SACL alone;
the descriptor kept the first state. The baseline showed it on Windows
Server 2022 and 2025, and an earlier run with the same two existing tests
showed it on Windows 11 26H1; the host (build 33438) reads both ways alike.
The baseline wasn't run on Server 2019 and Windows 11 22H2. `Write()` stores
the sections that were read, so a descriptor with the wrong flag would also
have stored the SACL as protected. Fixed in `962887a`: the descriptor takes
the audit section from a separate read, as it already did for the access
section.
2. **`Get-NTFSEffectiveAccess -ServerName ''`** wrote "Access is denied" on the
same machines, because Windows takes an empty name for this computer and the
remote interface then refuses the check; on the host it fails as
unreachable. Fixed in `962887a`: an empty name names no computer, so the
cmdlet warns and returns the result of this computer on every machine.
3. **`Get-NTFSEffectiveAccess` for a user who isn't an administrator**, on a
computer in a domain, wrote "Access is denied" and returned nothing for
every account, also for the default `-ServerName localhost`. See the probe
below. The function that fails, `GetEffectivePermissions_AuthzInitializeContextFromSid`,
is identical in the published 5.0.0-rc6 (compared with `git diff` against
the tag; the cmdlet wasn't run there), and the defect shows in the tests only
on a domain-joined machine as a basic user. Fixed in `fdd7a8b`.
### The probe
The remote interface of the authorization manager of a computer answers only
its administrators and the members of its group Access Control Assistance
Operators. A computer in a domain offers the interface to every caller; a
computer outside a domain doesn't, so the cmdlet already used the local manager
there. The probe, on Windows Server 2019, 2022, and 2025, with the module of
`962887a` (before the third fix):
| Token | Name of this computer (the default, `localhost`, computer name, FQDN) | `-ServerName ''` (the local manager) | Another computer |
| --- | --- | --- | --- |
| Lab account, elevated | Result for every account | Result for every account | Result for every domain account |
| Lab account, filtered (SAFER Normal User) | Access denied for every account | Result for every account, also the Administrator and Domain Users of the domain | Result for domain accounts: network authentication carries the groups of the account |
| Local standard user | Access denied | Result, except for the domain Administrator, a user account of the domain | Access denied: a local account has no domain credentials |
| Standard domain user | Access denied for every account | Result for every account, also the domain Administrator | Access denied, as the cmdlet page and the live test of the delegated account describe |
The accounts were the user itself, Everyone, the local Administrator, and the
Administrator and Domain Users of the domain. The cmdlet now uses the local
manager for a name of this computer when the remote one refuses the user. The
second column shows that this answers for every caller of these four kinds,
except for a local user who asks about a user account of the domain; that stays
an "Access is denied" from Windows. For another computer, the denial stays an
error. A new live test runs the case as the administrator of the file server,
who isn't an administrator of the client, and compares the rights with the S4U
oracle that the other roles use.
### In the environment
- The base images of Windows Server 2019 and Windows 11 22H2 had an empty EFI
system partition: the `bcdboot` of the Server 2025 host fails with exit code
193 on their boot files, and AutomatedLab ignores the exit code, so the VM
doesn't boot (Hyper-V event 18603). `Repair-OsMatrixBoot.ps1` runs the
`bcdboot` of the image itself on the VM's own disk.
- Windows 11 26H1 (28000.1836) joins the domain but loses the secure channel to
the domain controller (26100.32690): the client asks `NetrLogonGetCapabilities`
for query level 2, the controller answers `0xC0000022`, and the client denies
the channel (`NlConfirmRequestedCapabilities: denying access ... 0xc0000022`).
A rejoin can't fix a protocol mismatch, so this machine isn't a domain client.
- The Windows 11 Enterprise Evaluation image shuts itself down an hour after
each start (`wlms.exe`: "The license period for this installation of Windows
has expired"; status 0xC004F009). It recorded its install time on a clock that
ran about seven hours ahead, the clock was then corrected, and the evaluation
licensing took the step back as the end of the grace period. One of the two
documented rearms didn't clear it. After an unplanned shutdown its machine
account password no longer matched the domain's (`0xC000018D` for domain
logons, `nltest /sc_verify` reports `ERROR_INVALID_PASSWORD`);
`Test-ComputerSecureChannel -Repair` with the lab account repaired it. Runs on
this machine have to stay under an hour from its start.
- A profile of an account that a scheduled task used stayed loaded on one
server, so its folder couldn't be removed until the machine restarted.
### In the harness
- A line that a native command writes to stderr is a terminating error in
Windows PowerShell 5.1 under `$ErrorActionPreference = 'Stop'` when `2>&1`
redirects it. The first "The directory is not empty" of PowerShell 7 ended the
fixture removal on Server 2019 before the retry.
- `Get-LocalGroupMember` fails with "Failed to compare two elements in the
array" when a group holds an orphaned SID, which stopped the setup of the next
run. The setup now adds members and ignores `MemberExistsException`.
- A profile that is gone in the meantime failed the cleanup of the client.
All three are fixed in the controller (`7d47316`).
### The effective-access failures of the Admin role
In the cells of the Windows Server 2022 file server, and only there, the Admin
role failed two effective-access tests of case 3 in `rc7f`, `rc7h`, `rc7i`, and
`rc7j` (`rc7j` ran the candidate `962887a`; `rc7i` failed only in Windows
PowerShell): `Get-NTFSEffectiveAccess` returned no access (Synchronize only,
`0x100000`) for the subject of case 3, where the tests expect the rights
through the nested domain groups (`0x1200A9`, and `0x1201BF` with the local
group of the file server), either with the default `-ServerName` (the
authorization manager of the client) or with the name of the file server, or
both. The baseline had passed the same position in `rc7e` and `rc7k`, and the
audit read of `962887a` is the only change of the module on the path of the
cmdlet before `fdd7a8b`, so the module was the first suspect. It isn't the
cause, as the replay below shows. In `rc7c`, the failed cleanup of the first cell
had left the accounts in place through all three cells; in the later sequences
the fixture was removed after most cells and created again for the next one, with
the same names and new SIDs.
**The replay.** The controller of `db04ef2` (the controller of `rc7f` to
`rc7k`, blob `683aee91ec8805d77a33b2d368acaf876724fa32`, the fixed name of the
account), Windows PowerShell only, the file server OSFile22, seven cells (`ab0`
to `ab6`, 04:57 to 05:33 UTC) back to back after a restart of the client, the
module alternating between the baseline `83149ee` and the final
candidate `fdd7a8b`. The live tests were the blob `efe36e5073b9b10742ca7de242ddcbe90d8eda62`
with one test added for this replay (the file then has the blob
`72c12fe09e4005e048a8aed0fa02b9a922c58f34`), which isn't committed: after the
three effective-access tests of the Admin role it prints, in the same second, the
state of the subject account (see below); it runs after them, so it can't change
their results. `ab0` is the warm-up and has a new fixture.
| Cell | Module | Admin role at (UTC) | Minutes since the previous removal | Test 1, name of the file server | Test 2, default server name |
| --- | --- | --- | ---: | --- | --- |
| `ab0` | final | 05:00:57 | 54.0 | pass | pass |
| `ab1` | baseline | 05:06:09 | 3.8 | FAIL `0x100000` | FAIL `0x100000` |
| `ab2` | final | 05:11:12 | 3.7 | pass | pass |
| `ab3` | final | 05:16:23 | 3.9 | FAIL `0x100000` | FAIL `0x100000` |
| `ab4` | baseline | 05:21:34 | 3.9 | pass | pass |
| `ab5` | baseline | 05:26:57 | 3.9 | FAIL `0x100000` | FAIL `0x100000` |
| `ab6` | final | 05:32:04 | 3.8 | pass | pass |
In every cell from `ab1` on, the accounts were created 1.0 minute after the
removal of the previous fixture, and the Admin role ran 3.7 to 3.9 minutes
after it. The cells differ in the module, in the outcome, and in one more
variable: the age of the entry that an earlier cell left for the same account
name, counted from that cell's Admin role (5.2 to 5.4 minutes in the failing
cells, 10.25 to 10.5 minutes in the passing ones). Not counting the warm-up
`ab0`, the baseline fails two of its three cells and the final candidate one of
its three, and the failing and the passing cells alternate. If the module
decided, the baseline wouldn't fail.
**What is wrong in a failing cell.** The test that runs right after the three
tests printed the same in `ab1`, `ab3`, and `ab5`: the name `osmatrix\NtfsLiveSubject`
resolves to the current SID; a Kerberos S4U logon of `NtfsLiveSubject@osmatrix.net`
on the client returns the current SID with nine groups, among them `NtfsLiveInner`
and `NtfsLiveOuter` (this logon is the oracle of the controller); `Get-NTFSEffectiveAccess`
with the unreachable server name, which falls back to the local authorization
manager, returns `0x1200A9`; and every call that asks a remote authorization
manager, the one of the client by the default `-ServerName` and the one of the
file server by its name, returns `0x100000`, by name and by SID alike. In the
passing cells all five calls were right. So the remote authorization managers
answer as if the account had no groups, while the name resolution, the Kerberos
logon, and the local manager are right in the same second. The module makes the
same Authz calls for both kinds of manager; only the manager differs.
**A model that fits.** The pattern is the one of a cache. The model: a remote
authorization manager computes the groups of an account at the first request
for the account name and answers from that result for L minutes, also when the
account was deleted and created again under the same name in the meantime. The
file server and the client have one entry each for a name, and use doesn't
renew it. `Test-StaleAuthzModel.ps1` replays the Admin roles of the timeline of
all cells ([Timeline.csv](Acceptance-2026-10-10-os-matrix-Timeline.csv): `rc7c`
to `rc7l` and `ab0` to `ab10`, 43 runs in 27 cells, and `rc7d`, which stopped
before its tests) against the model (`-StepMinutes 0.05`, so every bound is known
to within 0.05 minute). With L from 9.35 to 10.25 minutes the model predicts the
result of the first test (the file server) of all 43 runs, and with L from 9.95
to 10.25 minutes that of the second (the client): 43 of 43 for each, with 6 and 9
failures. One L from 9.95 to 10.25 minutes serves both tests (86 of 86). That
includes the cells where the two tests differ (`rc7f`, `rc7h`: the entry of the
client was stale, the one of the file server had expired), the cells of the
baseline that passed (`rc7e`, `rc7k`), and the cells that passed with an account
name that was new. A random assignment of the observed outcomes to the runs (the
same number of failures) never fits that well: none of 5,000 assignments reaches
43 of 43 for any L, and the best of them reaches 41 for the first test and 39
for the second (`-Permutations 5000`, fixed seed). I fitted the model after
`ab3` and wrote down its predictions before they ran (in the night log of the
session, outside the repository, at 05:20 UTC): `ab4` passes, `ab5` fails, `ab6`
passes. All three held, and `ab5` is the baseline failing; if the module decided,
`ab5` would have passed and `ab6` would have failed. `ab6` is the weakest of the
three: its entry was 10.5 minutes old, a little above the lifetimes that fit.
**The probes of the night.** Three probes (the second is
`Probe-AccountRecreation.ps1` of the kit) deleted and created the accounts again
within seconds. In that regime, the Kerberos S4U logon itself returned the old
account on the domain controller, the client, and the file server for more than
seven and less than fifteen minutes, and both modules returned `0x100000` for
every call. In the cells, with one minute between the deletion and the new
creation, the Kerberos logon is right (the oracle of the controller never failed,
and the replay prints it). Both are state that Windows keeps for a name beyond
the deletion of the account; the cells show the variant of the remote
authorization managers.
The first loop probe, with the baseline and the final candidate:
| Round | Name resolves to | S4U token of the account on the client | Baseline and final candidate, by name and by SID, with the default `-ServerName` and with the file server |
| --- | --- | --- | --- |
| 1 (new names) | the current SID | holds the outer group | `0x1200A9`, both modules, all four calls |
| 2 to 6 | the SID of the previous round in the first process of a round, the current SID in the second | lacks the new outer group | `0x100000`, both modules, all four calls |
The probe of the kit, `Probe-AccountRecreation.ps1`, which also logs the user on
with Kerberos S4U on the domain controller, the client, and the file server, gave
the same picture in four rounds with the baseline and the final candidate (04:19
UTC): in round 1, all three machines returned the current account and both
modules `0x1200A9` for every call; in rounds 2 to 4, all three returned the old
account, without the new outer group, and both modules `0x100000` for every call.
The own ticket cache of the computers (logon session `0x3e7`) held no ticket for
the account, and a purge of it changed nothing.
A third probe created five sets of accounts, logged each user on with S4U on the
three machines, deleted and created them again with the same names within a
second, and asked once per set after a delay (the sets after the first were
asked after a `klist purge` on the client, so the rows of the client for them
aren't independent):
| Question | Domain controller | File server | Client |
| --- | --- | --- | --- |
| At once | old account | old account | old account; Authz by SID `0x100000` |
| After `klist purge` on the client | old account | old account | current account (the token of the session); Authz by SID still `0x100000` |
| After `nltest /sc_reset`, a DNS flush on the client, and a restart of the Kerberos service of the domain controller | old account | old account | unchanged |
| 60 seconds after the accounts were created again | old account | old account | Authz by SID `0x100000` |
| 180 seconds | old account | old account | Authz by SID `0x100000` |
| 420 seconds | old account | old account | Authz by SID `0x100000` |
| 900 seconds | current account | current account | Authz by SID `0x1200A9`, also with the name of the file server |
`WindowsIdentity` with the user principal name, which the module doesn't call,
returns the old account in this regime, so the module isn't involved in it
either.
**What the evidence supports.** The failures of the Admin role depend on the
position of the cell relative to the previous fixture with the same account
name, and the module doesn't decide the outcome: not counting the warm-up, the
baseline fails in two of its three replay cells and the final candidate in one
of its three, and one model with one parameter predicts all 43 runs, including
three that it predicted before they ran. In a failing cell the remote
authorization managers of the client and of the file server are the wrong layer:
the name resolution, a Kerberos logon of the account, and the local
authorization manager are right in the same second, and the module makes the
same Authz calls for both kinds of manager. The replay gives no reason to change
the module for it.
**What it doesn't establish.** How Windows does it: which component keeps the
state, and why for about ten minutes. L is estimated from 43 runs on one client
and three file servers with a cell every five minutes or so, so a different
spacing of the cells could tell more. The window of L that fits the client test
is 0.3 minute wide, and its bounds come from two runs (`rc7h`, whose Core run is
9.92 minutes after the entry of `rc7g`, and `ab2`, 10.25 minutes after `ab0`).
The times of the model are those of the start of the Admin role, some seconds
before the first request, and the offset may differ between the editions, so the
bounds of L are uncertain by about that much. The model describes the
observations that it was fitted to, and the three predictions are the only ones
that it didn't see. The replay rules out a module effect that decides the
outcome (every position that the model predicts to fail failed for the
baseline, the final candidate, and the baseline again, and every position that
it predicts to pass passed for the final candidate, the baseline, and the final
candidate), but six runs can't rule out a small or a random effect of the
module. The replay ran one edition against one file server. Whether a user can
meet it, an administrator who deletes an account, creates it again under the same
name, and asks within ten minutes for its effective access on a remote computer,
wasn't tried outside the lab. The cmdlet can't detect it: the answer of a manager
that has no groups for the account looks like the answer for an account without
access.
**The change of the controller.** A new fixture gets a new name for the account
of case 3 (`NtfsLiveSubject` and four digits, `1dec389`), and a fixture that
exists keeps its account. No cache has to be flushed, and the module isn't
changed by this. With it, the Windows Server 2022 cell passed in `rc7l`, where the
cells of the old controller had failed in `rc7f`, `rc7h`, `rc7i`, and `rc7j`, and
so did the other two cells of that sequence.
The controller of `1dec389` (blob `9917cac5820ed20ed2eb5592eff06894677f9874`)
then ran four more cells of the replay, `ab7` to `ab10`: baseline, final,
baseline, final, in Windows PowerShell against OSFile22, back to back after a
restart of the client (05:37 to 05:58 UTC), with the same diagnostic test. Each
cell created a fixture with a new name for the account of case 3. I wrote the
prediction down before the Admin role of `ab7` ran (night log, about 05:40 UTC):
all four pass. For a controller that reuses the name, the model with L = 10.1
minutes predicts failures in `ab7` (the entry of `ab6` would have been 8.1
minutes old) and in `ab9` (5.4 minutes after `ab8`):
| Cell | Module | Account of case 3 | Admin role at (UTC) | Test 1 | Test 2 | Test 3, local manager | The model, had the name been reused (`-AsIfSameSubject`) |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `ab7` | baseline | `NtfsLiveSubject8013` | 05:40:11 | pass | pass | pass | Test 1 FAIL; Test 2 FAIL, unless the restart of the client at 05:34 cleared its entry |
| `ab8` | final | `NtfsLiveSubject0900` | 05:45:32 | pass | pass | pass | pass |
| `ab9` | baseline | `NtfsLiveSubject7705` | 05:50:54 | pass | pass | pass | both tests FAIL |
| `ab10` | final | `NtfsLiveSubject8799` | 05:56:12 | pass | pass | pass | pass |
The model doesn't know about restarts, and the client restarted ten times between
23:37 and 05:34 UTC, nine of them after the first cell had started (Hyper-V worker
log, UTC: 23:37, 00:38, 01:43, 01:58, 02:43, 03:23,
03:42, 04:07, 04:55, and 05:34; the file servers and the domain controller
didn't restart between the first and the last run, except OSFile22 at 01:36).
If the entry of a remote manager lives in the memory of the computer, a restart
clears it. For the fit this changes no prediction: of the entries that the model
keeps, only one lives across a restart and is read by a later run (the entry
that `rc7e` made at 00:35:48 on OSFile25, read by its Core run after the restart
at 00:38), and that run has the same account, so it passes either way. For the
counterfactual it matters once, in the table: the restart at 05:34 came between
`ab6` and `ab7`, so the client test of `ab7` is a prediction only if the state
survives a restart, while the file-server test (OSFile22 didn't restart) is one
in any case.
In each cell the diagnostic test printed the right rights for all five calls
(`0x1200A9` for the client, `0x1201BF` for the file server). In the timeline, the
old controller failed in 7 of its 20 cells, all on OSFile22 (`rc7f`, `rc7h`,
`rc7i`, `rc7j`, `ab1`, `ab3`, `ab5`); the new one failed in none of its 7 (`rc7l`
and `ab7` to `ab10`), where the model for a reused name predicts failures in 3
(the OSFile22 cell of `rc7l`, `ab7`, `ab9`). The cells aren't paired runs and
seven cells are few, so this doesn't prove that the new names are the reason; it
shows that the failures are absent where the model says that a reused name
fails, which the reuse of the name explains and the module doesn't.
## Limits and open items
- Every run is a validation of a local build (`-ModulePath`). The acceptance of
a release is the run with `-Version` of the exact prerelease from the
PowerShell Gallery in every cell, which handoff 3 sequences after the maintainer
decides which fixes belong to 5.0.0-rc7. The same cells have to be repeated for
a changed binary. The `-Version` path of `Run-MatrixSequence.ps1` ran once as a
dry run with the published 5.0.0-rc6 on OSFile19 in Windows PowerShell (07:39 to
07:45 UTC, kit at `664ef3a`): the controller used the published module, the
validation reported `LIVE_RESULT_NOT_ACCEPTED` as it must (151 passed, 78
failed, 2 skipped: the live tests that rc6 predates, such as the later-command
tests, `Get-ChildItem2 -Filter`, `InheritedFrom`, and the two new ServerAdmin
tests), and the cleanup verdict was CLEAN. That tests the mechanics only and
accepts nothing.
- Case 9 (accounts of other domains and forests) needs trusts that the matrix
lab doesn't have; it runs only in `WindowsAccessControlLab`, where the
baseline passed it and the final candidate passed it in run `fl1` (see "First
lab, final candidate (case 9)"). The published package has to run there too.
- The file servers are Windows. A server of another kind is the subject of
Decision 23.
- Windows 11 26H1 has no domain cell until the domain controller or the
mismatch changes. The Windows 11 client of the cells is the 22H2 evaluation
build, which has to be started shortly before a run (see above).
- `GetEffectiveAccess` ignores what the initialization of the resource manager
throws (an outer `catch { }` that is older than this work). An operating
system that refused another computer at that step, not at the context as every
machine of the matrix did, would give a result without rights and a warning
instead of the documented error; and a failure of the local fallback for a name
of this computer would give a result without rights and neither a warning nor an
error, because the flag that suppresses the warning is set before the fallback
runs (also older than this work). The help and the CHANGELOG say that the error
stays for another computer, which holds for the denial at the creation of the
context. This is unverified on every machine of the matrix and outside the
fixes (Decision 16 asks for reproducible defects only); recording the
initialization exceptions in `authzException` would close it.
- The built-in `security-review` agent (the custom `security-reviewer` couldn't
start: its model isn't offered, and I didn't override it) read `83149ee..664ef3a`
and found no exploitable vulnerability in the changes of the module: the
decision "this name is this computer" is an exact, case-insensitive match
(true for `.`, `localhost`, the machine name, the host name, and the name with
the DNS domain; false for null, empty, whitespace, a trailing dot, an IP
address, UNC forms, an embedded NUL, and a name of 100,000 characters, all of
which keep the remote path and its denial), the fallback runs in the caller's
own process and token, and the separate audit read uses the privilege handling
of the combined read. It reported two LOW items. The first is the item above.
The second is that the kit creates staging folders directly under `C:\`
(`C:\NTFSSecurityLab`, `C:\NtfsMatrixLocal`, `C:\NtfsMatrixProbe`,
`C:\NtfsProbeModules`) without an ACL, so they inherit Authenticated Users:
Modify, while scripts and the module's DLL in them run elevated or as the role
accounts: a principal that can run code on a lab VM during a run could replace
them. The labs are isolated and the role accounts must read the tests and write
results there, so protecting the folders is a design change of the controller
that needs a new acceptance; I left it for the maintainer. The reviewer also
noted that the lab password crosses remoting and `Register-ScheduledTask
-Password` (module or script-block logging on a machine would record it), that
the controller reaches the client with CredSSP to an IP address, where the
logon inside CredSSP is NTLM-only and the server isn't authenticated (the
policy comes from AutomatedLab), and that the help says that a user who isn't an
administrator gets the result on a domain computer without saying that a local
standard user who asks about a domain account still gets "Access is denied" (the
probe table above) or that the answer now comes from the caller's own token and
manager. It could not check the ACL of `C:\` on the lab VMs, the behavior of the
fallback as a non-administrator on a domain computer, or whether the local
manager equals the remote one for every token.
- The scripts of the kit were read by a reviewer who ran none of them; module
logging or script-block logging on a machine would record the lab password
that `Register-ScheduledTask -Password` needs.
- The mechanism isn't known. The replay shows that the remote authorization
managers answer for an account name from state that outlives the account, and
the model puts the lifetime at about ten minutes (9.95 to 10.25 minutes for
both tests, from 43 runs), but I didn't find which component keeps it, why that
long, or whether it is constant: all runs have the same timing, and it was
fitted to them. The replay ran one edition (Windows PowerShell, Desktop)
against one file server (OSFile22). The probes of the first regime (accounts
deleted and created again within seconds), in which the Kerberos S4U logon
returned the old account for more than seven and less than fifteen minutes,
were measured once, with no repetition, and five remedies (`klist purge`,
`nltest /sc_reset`, a DNS flush, a restart of the Kerberos service of the
domain controller, and waiting) were tried: only waiting helped. A script of
the kit that creates accounts again under one name would meet the state; the
controller doesn't any more.
- The end-state check of the matrix reported the staging folders of the suite
runs (`C:\NtfsMatrixLocal`) as residue in the three cells of `rc7l`, which
made their verdict DIRTY, although the fixture was gone. The suite runner now
removes its stage after it has copied the results back. The check counts the
items in the stage folders, each of the folders `C:\NtfsProbeRecreation` and
`C:\NtfsProbeModules` that exists, the scheduled tasks of the matrix, the local
`NtfsProbe*` users, their profiles and profile folders (`C:\Users\NtfsProbe*`),
their entries in Performance Log Users, and the `NtfsProbe*` objects of the
directory, and `-Mode Repair` removes what it finds. `Verify` and `Repair` treat
every unresolved `S-1-5-21-…` member of Performance Log Users as the probe's
(the probe is the only writer of that group in these labs, and its own cleanup
uses the same pattern); on a machine where something else leaves such members,
the check would report them and `Repair` would remove them. A `Verify` on the
two machines of the first lab (07:29 UTC) found none. The check ran with real
residue on the five machines three times: at 06:03 UTC with the script that
`9344ff7` committed at 06:08 UTC, at
06:44 UTC with the handling of profiles and of the entries in Performance Log
Users that the follow-up review asked for, and at 06:51 UTC after the second run
had shown that the check missed the entry of a local user (`net localgroup`
lists a local user by its bare name, and the pattern wanted a domain prefix). A
first attempt at 05:58 UTC died while it made the residue, without a log (the
cause is unknown; decision log D37), so the run at 06:03 started in a lab where
that attempt might have made some items; its first `Verify` listed exactly the
expected ones.
The last run made residue of every kind at once: a stage item and a local user
`NtfsProbeDummy` on OSFile19; a local user with a profile and an entry in
Performance Log Users on OSFile19; a local user with a profile that was deleted
afterwards (an orphaned profile) on OSFile22; `C:\NtfsProbeRecreation` on
OSFile22; a domain account that was made a member of Performance Log Users on
OSFile22 and then deleted in the directory (`net localgroup` still showed it by
its cached name); a scheduled task `NtfsMatrix-dummy` on OSFile25;
`C:\NtfsProbeModules` on OSWin11E; and a disabled directory user
`NtfsProbeDummy`. `Verify` counted every item (on OSFile19 `probe users=2 probe
profiles=1 probe group members=1`, on OSFile22 `probe profiles=1 probe group
members=1`, and so on), and the verdict expression of `Run-MatrixSequence.ps1`,
read from the script with the parser and not copied, gave DIRTY. `Repair`
removed every item, and a second `Verify` gave CLEAN. Not tried: a profile that
stays loaded (the retries of `Repair` never needed a second attempt), and an
entry that `net localgroup` shows as a bare SID, so the branch for a SID and
`Remove-LocalGroupMember` with a SID didn't meet real residue (the cached name
of the deleted domain account was removed by name).
- Decision 24 is the agent's decision under the maintainer's delegation and stays
`proposed`. So do Decisions 22 and 23.
## Evidence
The raw logs, result files, probe outputs, and the packages are local, outside
Git, in the session files of the run; they aren't part of this commit. The
tables of this record are in the files next to it:
- [the suite results of the three candidates](Acceptance-2026-10-10-os-matrix-LocalSuite.csv),
- [the failing tests of every suite run](Acceptance-2026-10-10-os-matrix-Failures.csv),
- [the controller cells of `rc7c` to `rc7l`](Acceptance-2026-10-10-os-matrix-Cells.csv),
- [the timeline of the Admin role of every cell and edition, `rc7c` to `rc7l` and the replay `ab0` to `ab10`, with the three effective-access tests](Acceptance-2026-10-10-os-matrix-Timeline.csv),
- [the counts of the first-lab run `fl1`](Acceptance-2026-10-10-os-matrix-FirstLab.csv).
The scripts that produced them are in [Acceptance](Acceptance), and the
decision is `.memory-bank\decisions\0024-os-matrix-lab.md`.

125
Tests/Lab/Acceptance/Add-OsMatrixMachine.ps1

@ -0,0 +1,125 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $LogPath,
[Parameter(Mandatory)] [ValidatePattern('^[A-Za-z][A-Za-z0-9-]{0,14}$')] [string] $Name,
[Parameter(Mandatory)] [string] $OperatingSystem,
[Parameter(Mandatory)] [string] $IpAddress,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[ValidateRange(2, 16)] [int] $MemoryGB = 4,
[ValidateRange(1, 8)] [int] $Processors = 2,
[ValidateRange(5, 240)] [int] $StartTimeoutMinutes = 40,
[string] $BackupRoot = 'C:\ProgramData\AutomatedLab\Backups'
)
# Adds one machine to the already deployed matrix lab (Decision 24) and creates only that machine. AutomatedLab 5.61 has no supported way to
# extend a deployed lab: Add-LabMachineDefinition refuses while a lab is imported or exported, and Install-Lab creates every machine of the
# lab again. This script copies the lab metadata first (the copy is readable by administrators only, because the files hold the lab
# credentials), reloads the definition with Import-LabDefinition (never Import-Lab), adds the machine, exports the definition, and then runs
# the same steps Install-Lab runs for a single machine: base image, hosts entries, virtual machine, start. The other machines are neither
# created, started, nor changed. Windows PowerShell 5.1 on the host; run it elevated.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script must run in an elevated PowerShell session.' }
($stamp -f [DateTime]::UtcNow) + " START add-os-matrix-machine lab=$LabName name=$Name os='$OperatingSystem' ip=$IpAddress" | Set-Content -LiteralPath $LogPath
$lockPath = $null
try {
Import-Module -Name AutomatedLab -ErrorAction Stop
if ((Get-Lab -List) -notcontains $LabName) { throw "The lab '$LabName' does not exist." }
if (Get-VM -Name $Name -ErrorAction SilentlyContinue) { throw "A virtual machine named '$Name' exists already." }
$hostsText = Get-Content -LiteralPath (Join-Path -Path $env:SystemRoot -ChildPath 'System32\drivers\etc\hosts') -Raw
if ($hostsText -match ('(?im)^\s*[^#\s]+\s+{0}(\.|\s|$)' -f [regex]::Escape($Name)) -or $hostsText -match ('(?im)^\s*{0}\s' -f [regex]::Escape($IpAddress))) {
throw "The hosts file mentions '$Name' or $IpAddress already."
}
$labFolder = Join-Path -Path (Get-LabConfigurationItem -Name LabAppDataRoot) -ChildPath "Labs\$LabName"
$backup = Join-Path -Path $BackupRoot -ChildPath ('{0}-{1:yyyyMMdd-HHmmss}' -f $LabName, [DateTime]::UtcNow)
$null = New-Item -ItemType Directory -Path $backup -Force
$null = & icacls.exe $backup /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F'
if ($LASTEXITCODE -ne 0) { throw "icacls failed on the backup folder (exit code $LASTEXITCODE)." }
Copy-Item -LiteralPath $labFolder -Destination $backup -Recurse
Write-Step "lab metadata copied to $backup"
Import-LabDefinition -Name $LabName
$definition = Get-LabDefinition
$before = @(Get-LabMachineDefinition | ForEach-Object -Process { $_.Name })
$domainName = $definition.Domains[0].Name
$rootDc = Get-LabMachineDefinition | Where-Object -FilterScript { 'RootDC' -in $_.Roles.Name } | Select-Object -First 1
$dcAddress = ($rootDc.NetworkAdapters | Select-Object -First 1).Ipv4Address.IpAddress.AddressAsString
$dcPrefix = ($dcAddress -split '\.')[0..2] -join '.'
$newPrefix = ($IpAddress -split '\.')[0..2] -join '.'
if ($dcPrefix -ne $newPrefix) { throw "$IpAddress isn't in the /24 of the domain controller ($dcAddress)." }
Write-Step ("definition loaded: domain {0}; machines {1}; installation account {2}" -f $domainName, ($before -join ','), $definition.DefaultInstallationCredential.UserName)
$parameters = @{
Name = $Name; DomainName = $domainName; OperatingSystem = $OperatingSystem; Memory = ($MemoryGB * 1GB)
Processors = $Processors; Network = $LabName; IpAddress = $IpAddress
}
if ($OperatingSystem -like 'Windows 11*') {
$parameters.HypervProperties = @{ EnableSecureBoot = 'On'; SecureBootTemplate = 'MicrosoftWindows'; EnableTpm = 'true' }
}
Add-LabMachineDefinition @parameters
Export-LabDefinition -Force -ExportDefaultUnattendedXml
Import-Lab -Name $LabName -NoValidation -NoDisplay
$after = @(Get-LabVM -IncludeLinux | ForEach-Object -Process { $_.Name })
$difference = @(Compare-Object -ReferenceObject ($before + $Name) -DifferenceObject $after)
if ($difference.Count -gt 0) { throw "The exported lab doesn't hold exactly the old machines plus $Name. Restore the metadata from $backup." }
Write-Step 'definition extended and exported'
$lockCandidate = Get-LabConfigurationItem -Name DiskDeploymentInProgressPath
if (Test-Path -LiteralPath $lockCandidate) { throw "Another lab disk deployment seems to be in progress ($lockCandidate)." }
$null = New-Item -Path $lockCandidate -ItemType File -Value $LabName
# Only a lock that this script created is removed in the finally block below.
$lockPath = $lockCandidate
New-LabBaseImages
Write-Step 'base images ready'
$machine = Get-LabVM -ComputerName $Name
$address = ($machine.NetworkAdapters | Select-Object -First 1).Ipv4Address.IpAddress.AddressAsString
$null = Add-HostEntry -HostName $machine.Name -IpAddress $address -Section $LabName
$null = Add-HostEntry -HostName $machine.FQDN -IpAddress $address -Section $LabName
New-LabVM -Name $Name
Set-LabDefinition -Machines (Get-Lab).Machines
Export-LabDefinition -Force -ExportDefaultUnattendedXml -Silent
Write-Step 'virtual machine created and definition exported'
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
$lockPath = $null
Start-LabVM -ComputerName $Name -ProgressIndicator 30 -TimeoutInMinutes $StartTimeoutMinutes -Wait
Write-Step 'machine started and reachable with the lab credentials'
$userName = (Get-Lab).DefaultInstallationCredential.UserName
Invoke-LabCommand -ActivityName 'Setting PasswordNeverExpires for local deployment accounts' -ComputerName $Name -NoDisplay -Variable (Get-Variable -Name userName) -ScriptBlock {
Get-CimInstance -Query "Select * from Win32_UserAccount where name = '$userName' and localaccount='true'" | Set-CimInstance -Property @{ PasswordExpires = $false }
}
$evidence = Invoke-LabCommand -ComputerName $Name -ActivityName 'Readiness of the new member' -NoDisplay -PassThru -ErrorAction Stop -ArgumentList $domainName -ScriptBlock {
param ($Domain)
$current = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
[pscustomobject]@{
Build = '{0}.{1}' -f $current.CurrentBuildNumber, $current.UBR
Product = $current.ProductName
Domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
SecureChannel = [bool] (Test-ComputerSecureChannel)
Verify = (@(& nltest.exe "/sc_verify:$Domain" 2>&1) -join ' | ')
}
}
Write-Step ('new member: build {0} ({1}); domain {2}; secure channel {3}; nltest: {4}' -f $evidence.Build, $evidence.Product, $evidence.Domain, $evidence.SecureChannel, $evidence.Verify)
if (-not $evidence.SecureChannel) { throw "The secure channel of $Name is broken." }
Write-Step 'add-os-matrix-machine-DONE'
exit 0
}
catch {
Write-Step ('add-os-matrix-machine-FAILED: {0}' -f $_)
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
exit 1
}
finally {
if ($lockPath) { Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue }
}

91
Tests/Lab/Acceptance/Complete-OsMatrixLab.ps1

@ -0,0 +1,91 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $LogPath,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string[]] $Member = @('OSDC1', 'OSFile19', 'OSFile22', 'OSFile25', 'OSWin11E'),
[string[]] $LocalCredentialMember = @(),
[string] $PesterModulePath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1',
[string] $PowerShell7Msi = 'V:\LabSources\SoftwarePackages\PowerShell-7.6.3-win-x64.msi'
)
# Finishes machines of the matrix lab after a deployment that stopped in AutomatedLab's file server step (a job that never completed
# although its remote side was idle) or after Add-OsMatrixMachine.ps1: detaches the installation ISO from the file servers, installs
# PowerShell 7 from the MSI of the host, and copies Pester 5.7.1 into the module folders of both editions. It uses no AutomatedLab job
# (no -AsJob), only synchronous remoting. A member in -LocalCredentialMember is reached with the local installation account through a
# session, for a machine whose secure channel to the domain controller fails. Windows PowerShell 5.1 on the host; run it elevated.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$Member = @($Member | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$LocalCredentialMember = @($LocalCredentialMember | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
$installBlock = {
param ($Msi)
$msiPath = Join-Path -Path 'C:\Windows\Temp' -ChildPath $Msi
$process = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', ('"{0}"' -f $msiPath), '/quiet', '/norestart', 'ADD_PATH=1', '/l*v', 'C:\Windows\Temp\pwsh-install.log') -Wait -PassThru
$pwsh = Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe'
[pscustomobject]@{ ExitCode = $process.ExitCode; Pwsh = $(if (Test-Path -LiteralPath $pwsh) { (Get-Item -LiteralPath $pwsh).VersionInfo.ProductVersion } else { 'missing' }) }
}
$createBlock = { param ($Path) $null = New-Item -Path $Path -ItemType Directory -Force }
$checkBlock = { param ($Path) '{0}: {1}' -f $env:COMPUTERNAME, (Test-Path -LiteralPath (Join-Path -Path $Path -ChildPath '5.7.1\Pester.psd1')) }
($stamp -f [DateTime]::UtcNow) + " START complete-os-matrix-lab lab=$LabName members=$($Member -join ',') localCredential=$($LocalCredentialMember -join ',')" | Set-Content -LiteralPath $LogPath
try {
foreach ($path in $PesterModulePath, $PowerShell7Msi) { if (-not (Test-Path -LiteralPath $path)) { throw "Missing payload: $path" } }
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
$fileServers = @($Member | Where-Object -FilterScript { $_ -like 'OSFile*' -and $_ -notin $LocalCredentialMember })
if ($fileServers) {
Dismount-LabIsoImage -ComputerName $fileServers -SupressOutput
Write-Step "installation ISO detached from $($fileServers -join ',')"
}
$msiName = Split-Path -Path $PowerShell7Msi -Leaf
$domainMembers = @($Member | Where-Object -FilterScript { $_ -notin $LocalCredentialMember })
foreach ($name in $Member) {
if ($name -in $LocalCredentialMember) {
$session = New-LabPSSession -ComputerName $name -UseLocalCredential
try {
Copy-Item -LiteralPath $PowerShell7Msi -Destination 'C:\Windows\Temp\' -ToSession $session -Force
$outcome = Invoke-Command -Session $session -ScriptBlock $installBlock -ArgumentList $msiName
foreach ($modulesRoot in 'C:\Program Files\WindowsPowerShell\Modules', 'C:\Program Files\PowerShell\Modules') {
$destination = Join-Path -Path $modulesRoot -ChildPath 'Pester'
Invoke-Command -Session $session -ScriptBlock $createBlock -ArgumentList $destination
Copy-Item -LiteralPath $PesterModulePath -Destination $destination -ToSession $session -Recurse -Force
Write-Step ("Pester 5.7.1 in {0}: {1}" -f $modulesRoot, (Invoke-Command -Session $session -ScriptBlock $checkBlock -ArgumentList $destination))
}
}
finally {
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}
}
else {
Copy-LabFileItem -Path $PowerShell7Msi -ComputerName $name -DestinationFolderPath 'C:\Windows\Temp'
$outcome = Invoke-LabCommand -ComputerName $name -ActivityName "Install PowerShell 7 on $name" -NoDisplay -PassThru -ErrorAction Stop -ArgumentList $msiName -ScriptBlock $installBlock
}
Write-Step ("PowerShell 7 on {0}: msiexec exit code {1}; pwsh {2}" -f $name, $outcome.ExitCode, $outcome.Pwsh)
if ($outcome.ExitCode -notin 0, 3010 -or $outcome.Pwsh -eq 'missing') { throw "PowerShell 7 isn't installed on $name (exit code $($outcome.ExitCode))." }
}
if ($domainMembers) {
foreach ($modulesRoot in 'C:\Program Files\WindowsPowerShell\Modules', 'C:\Program Files\PowerShell\Modules') {
$destination = Join-Path -Path $modulesRoot -ChildPath 'Pester'
Invoke-LabCommand -ComputerName $domainMembers -ActivityName 'Create the Pester module directory' -NoDisplay -ErrorAction Stop -ArgumentList $destination -ScriptBlock $createBlock
Copy-LabFileItem -Path $PesterModulePath -ComputerName $domainMembers -DestinationFolderPath $destination -Recurse
$found = Invoke-LabCommand -ComputerName $domainMembers -ActivityName 'Check Pester' -NoDisplay -PassThru -ErrorAction Stop -ArgumentList $destination -ScriptBlock $checkBlock
Write-Step ("Pester 5.7.1 in {0}: {1}" -f $modulesRoot, (@($found) -join '; '))
if (@($found | Where-Object -FilterScript { $_ -notmatch ': True$' }).Count -gt 0) { throw "Pester 5.7.1 isn't in $destination on every member." }
}
}
Write-Step 'complete-os-matrix-lab-DONE'
exit 0
}
catch {
Write-Step ("complete-os-matrix-lab-FAILED: {0}" -f $_)
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
exit 1
}

105
Tests/Lab/Acceptance/Deploy-OsMatrixLab.ps1

@ -0,0 +1,105 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $LogPath,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string] $DomainName = 'osmatrix.net',
[string] $VmPath = 'V:\AutomatedLab-VMs',
[string] $AddressSpace = '192.168.12.0/24',
[string] $PesterModulePath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1',
[string] $PowerShell7Msi = 'V:\LabSources\SoftwarePackages\PowerShell-7.6.3-win-x64.msi'
)
# Deploys an isolated AutomatedLab lab for the NTFSSecurity operating-system matrix (Decision 24): one domain controller, three file
# servers (Server 2019, 2022, 2025), and a Windows 11 client, in a domain and on a switch of their own. It touches none of the
# existing labs, machines, switches, or domains, never calls Remove-Lab, and refuses to run when the lab or a machine name exists.
# The installation password is generated here, kept in memory, and stored only where AutomatedLab stores it for every lab.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script must run in an elevated PowerShell session.' }
($stamp -f [DateTime]::UtcNow) + " START deploy-os-matrix-lab lab=$LabName" | Set-Content -LiteralPath $LogPath
try {
Import-Module -Name AutomatedLab -ErrorAction Stop
foreach ($path in $PesterModulePath, $PowerShell7Msi) { if (-not (Test-Path -LiteralPath $path)) { throw "Missing payload: $path" } }
$machines = @(
@{ Name = 'OSDC1'; Os = 'Windows Server 2025 Datacenter (Desktop Experience)'; Roles = @('RootDC'); Memory = 4GB; Address = '192.168.12.10' }
@{ Name = 'OSFile25'; Os = 'Windows Server 2025 Datacenter (Desktop Experience)'; Roles = @('FileServer'); Memory = 3GB; Address = '192.168.12.25' }
@{ Name = 'OSFile22'; Os = 'Windows Server 2022 Datacenter (Desktop Experience)'; Roles = @('FileServer'); Memory = 3GB; Address = '192.168.12.22' }
@{ Name = 'OSFile19'; Os = 'Windows Server 2019 Datacenter (Desktop Experience)'; Roles = @('FileServer'); Memory = 3GB; Address = '192.168.12.19' }
@{ Name = 'OSWin11'; Os = 'Windows 11 Pro'; Roles = @(); Memory = 4GB; Address = '192.168.12.11' }
)
# Collision checks from AutomatedLab metadata and from Hyper-V; the existing labs are only read.
$existingNames = New-Object System.Collections.Generic.List[string]
$labs = @(Get-Lab -List)
if ($labs -contains $LabName) { throw "The lab '$LabName' exists already. Refusing to redefine it." }
foreach ($existing in $labs) {
Import-Lab -Name $existing -NoValidation -NoDisplay -ErrorAction Stop
foreach ($vm in Get-LabVM -IncludeLinux) { $existingNames.Add($vm.Name) }
}
foreach ($vm in Get-VM) { $existingNames.Add($vm.Name) }
$collisions = @($machines.Name | Where-Object { $_ -in $existingNames })
if ($collisions) { throw "Machine name collision: $($collisions -join ', ')" }
if (Get-VMSwitch -Name $LabName -ErrorAction SilentlyContinue) { throw "A virtual switch named '$LabName' exists already." }
$usedAddresses = @(Get-NetIPAddress -AddressFamily IPv4 | ForEach-Object { $_.IPAddress })
if ($usedAddresses | Where-Object { $_ -like '192.168.12.*' }) { throw 'The address space 192.168.12.0/24 is in use on the host.' }
Write-Step ('preflight ok; existing labs: {0}; existing machine names: {1}' -f ($labs -join ', '), $existingNames.Count)
$characters = ([char[]](48..57) + [char[]](65..90) + [char[]](97..122) + '!', '#', '%', '+', '-', '=')
# A cryptographic generator, without the bias of a remainder: this is the installation and domain administrator password of the lab.
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
$limit = 256 - (256 % $characters.Count)
$buffer = New-Object -TypeName 'byte[]' -ArgumentList 1
$chosen = New-Object -TypeName 'System.Text.StringBuilder'
while ($chosen.Length -lt 24) {
$generator.GetBytes($buffer)
if ($buffer[0] -lt $limit) { $null = $chosen.Append($characters[$buffer[0] % $characters.Count]) }
}
$password = 'Aa1!' + $chosen.ToString()
New-LabDefinition -Name $LabName -DefaultVirtualizationEngine HyperV -VmPath $VmPath
Add-LabVirtualNetworkDefinition -Name $LabName -AddressSpace $AddressSpace
Add-LabDomainDefinition -Name $DomainName -AdminUser 'install' -AdminPassword $password
Set-LabInstallationCredential -Username 'install' -Password $password
foreach ($definition in $machines) {
$parameters = @{
Name = $definition.Name; DomainName = $DomainName; OperatingSystem = $definition.Os; Memory = $definition.Memory
Processors = 2; Network = $LabName; IpAddress = $definition.Address
}
if ($definition.Roles.Count -gt 0) { $parameters.Roles = $definition.Roles }
if ($definition.Os -like 'Windows 11*') {
$parameters.HypervProperties = @{ EnableSecureBoot = 'On'; SecureBootTemplate = 'MicrosoftWindows'; EnableTpm = 'true' }
}
Add-LabMachineDefinition @parameters
}
Write-Step 'lab defined; installing network switches and base images'
Install-Lab -NetworkSwitches -BaseImages
Write-Step 'network switches and base images done'
Install-Lab
Write-Step 'machines, domain, and roles done'
$labMachines = Get-LabVM
Install-LabSoftwarePackage -ComputerName $labMachines -Path $PowerShell7Msi -CommandLine '/quiet /norestart ADD_PATH=1' -Timeout 30
Write-Step 'PowerShell 7 installed'
foreach ($modulesRoot in 'C:\Program Files\WindowsPowerShell\Modules', 'C:\Program Files\PowerShell\Modules') {
$destination = Join-Path $modulesRoot 'Pester'
Invoke-LabCommand -ComputerName $labMachines -ActivityName 'Create the Pester module directory' -ScriptBlock { param ($Path) $null = New-Item -Path $Path -ItemType Directory -Force } -ArgumentList $destination -NoDisplay
Copy-LabFileItem -Path $PesterModulePath -ComputerName $labMachines -DestinationFolderPath $destination -Recurse
}
Write-Step 'Pester 5.7.1 copied'
Show-LabDeploymentSummary -Summary
Write-Step "deploy-os-matrix-lab-DONE"
exit 0
}
catch {
Write-Step ("deploy-os-matrix-lab-FAILED: {0}" -f $_)
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
exit 1
}

131
Tests/Lab/Acceptance/Export-CellTimeline.ps1

@ -0,0 +1,131 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $MatrixRoot,
[Parameter(Mandatory)] [string[]] $Label,
[Parameter(Mandatory)] [string] $OutputPath
)
# The timeline of the cells of the operating-system matrix (Decision 24): for each cell and edition in which the Admin role ran, in the order in which the
# cells ran, the module under test, the account of case 3 (its name and its relative ID, which tells two accounts of one name apart within a domain),
# whether the previous cell had the same name and the same account, when the previous fixture was removed, when the accounts were created, when the
# Admin role started, the minutes between them, and the three effective-access tests of case 3 in the Admin role (result, milliseconds, and the rights
# that a failing test received). A failing effective-access test of the Admin role is easy to blame on the module or on the environment; this table
# puts it beside the module, the position of the cell in the sequence, and the age of the accounts. Pass every label of a series, also a run that
# stopped before its tests (it writes no row, but it created and removed the accounts, which the next cell reports as the previous removal). The
# times come from the logs of Run-MatrixSequence.ps1 and of the controller (UTC). It reads files only; Windows PowerShell 5.1 or PowerShell 7.
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$Label = @($Label | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
function Get-LogTime {
param ([string[]] $Lines, [string] $Pattern)
$line = $Lines | Where-Object -FilterScript { $_ -match $Pattern } | Select-Object -First 1
if ($line -and $line -match '^\[(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)Z?\]') {
[DateTime]::ParseExact($Matches[1], 'yyyy-MM-dd HH:mm:ss', [Globalization.CultureInfo]::InvariantCulture)
}
}
$cells = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ($name in $Label) {
$sequenceLog = Join-Path -Path $MatrixRoot -ChildPath ('{0}-sequence.log' -f $name)
if (-not (Test-Path -LiteralPath $sequenceLog)) { continue }
$candidate = if ((Get-Content -LiteralPath $sequenceLog -TotalCount 1) -match 'candidate-(\w+)') { $Matches[1] } else { '' }
foreach ($folder in (Get-ChildItem -LiteralPath $MatrixRoot -Directory -Filter ('{0}-*' -f $name))) {
$runLog = Join-Path -Path $folder.FullName -ChildPath 'run.log'
if (-not (Test-Path -LiteralPath $runLog)) { continue }
$run = @(Get-Content -LiteralPath $runLog)
$removeLog = Join-Path -Path $folder.FullName -ChildPath 'cleanup-2-remove.log'
$removed = if (Test-Path -LiteralPath $removeLog) { Get-LogTime -Lines @(Get-Content -LiteralPath $removeLog) -Pattern 'Removed the live tests' }
$configuration = Get-ChildItem -LiteralPath (Join-Path -Path $folder.FullName -ChildPath 'Results') -Recurse -Filter 'local-*.json' -ErrorAction SilentlyContinue |
Where-Object -FilterScript { $_.Name -match '-\d{14}\.json$' } | Select-Object -First 1
$subjectName = ''
$subjectRid = ''
if ($configuration) {
$subject = (Get-Content -LiteralPath $configuration.FullName -Raw | ConvertFrom-Json).Accounts.Subject
$subjectName = $subject.Name
$subjectRid = ($subject.Sid -split '-')[-1]
}
else {
# A cell that stopped before its tests has no result file, but it created and removed the accounts, so the snapshot of its fixture names them.
$snapshotLog = Join-Path -Path $folder.FullName -ChildPath 'cleanup-1-snapshot.log'
$snapshot = if (Test-Path -LiteralPath $snapshotLog) { Get-Content -LiteralPath $snapshotLog -Raw }
if ($snapshot -match '(?m)^(\w+)\.\S+\s+OU NTFSSecurityLive.*\b(NtfsLiveSubject\w*)=S-[\d-]+-(\d+)') {
$subjectName = '{0}\{1}' -f $Matches[1], $Matches[2]
$subjectRid = $Matches[3]
}
}
$cells.Add([pscustomobject]@{
Run = $name
Candidate = $candidate
FileServer = $folder.Name.Substring($name.Length + 1)
Folder = $folder.FullName
Lines = $run
Subject = $subjectName
SubjectRid = $subjectRid
Started = Get-LogTime -Lines $run -Pattern 'START live tests'
Created = Get-LogTime -Lines $run -Pattern 'Preparing the accounts'
Removed = $removed
})
}
}
$rows = New-Object -TypeName 'System.Collections.Generic.List[object]'
$previous = $null
foreach ($cell in ($cells | Sort-Object -Property Started)) {
foreach ($edition in 'Desktop', 'Core') {
$adminStart = Get-LogTime -Lines $cell.Lines -Pattern "local-$edition-\d+: role Admin$"
if (-not $adminStart) { continue }
$tests = @{ T1 = ''; T2 = ''; T3 = '' }
$failures = 0
$adminLog = Get-ChildItem -LiteralPath (Join-Path -Path $cell.Folder -ChildPath 'Results') -Recurse -Filter "local-$edition-*-Admin.log" | Select-Object -First 1
if ($adminLog) {
$text = @(Get-Content -LiteralPath $adminLog.FullName)
$start = ($text | Select-String -Pattern 'Describing Get-NTFSEffectiveAccess for a domain account on a share folder' | Select-Object -First 1).LineNumber
$seen = 0
for ($index = $start; $start -and $index -lt $text.Count -and $seen -lt 3; $index++) {
if ($text[$index] -notmatch '^\s+\[([+-])\] (.+?) (\d+(?:\.\d+)?m?s) \(') { continue }
$outcome = $Matches[1]; $title = $Matches[2]; $duration = $Matches[3]
$received = ''
if ($outcome -eq '-') {
$failures++
for ($next = $index + 1; $next -lt [Math]::Min($index + 12, $text.Count); $next++) {
if ($text[$next] -match "But was:\s+'(0x\w+)'") { $received = ' ' + $Matches[1]; break }
if ($text[$next] -match 'Expected \$null or empty') { $received = ' errors'; break }
}
}
$key = if ($title -match 'with -ServerName, without') { 'T1' } elseif ($title -match 'without -ServerName') { 'T2' } else { 'T3' }
$tests[$key] = '{0} {1}{2}' -f $(if ($outcome -eq '+') { 'pass' } else { 'FAIL' }), $duration, $received
$seen++
}
}
$hasPrevious = $null -ne $previous -and $null -ne $previous.Removed
$sameName = $null -ne $previous -and $cell.Subject -and $previous.Subject -eq $cell.Subject
$rows.Add([pscustomobject][ordered]@{
Run = $cell.Run
Candidate = $cell.Candidate
FileServer = $cell.FileServer
Edition = $edition
Subject = $cell.Subject
SubjectRid = $cell.SubjectRid
SameNameAsPreviousCell = [bool] $sameName
SameAccountAsPreviousCell = [bool] ($sameName -and $previous.SubjectRid -eq $cell.SubjectRid)
PreviousRemoval = $(if ($hasPrevious) { '{0:yyyy-MM-dd HH:mm:ss}' -f $previous.Removed })
AccountsCreated = '{0:yyyy-MM-dd HH:mm:ss}' -f $cell.Created
AdminRoleStarted = '{0:yyyy-MM-dd HH:mm:ss}' -f $adminStart
MinutesRemovalToCreation = $(if ($hasPrevious) { '{0:N1}' -f ($cell.Created - $previous.Removed).TotalMinutes })
MinutesCreationToAdmin = '{0:N1}' -f ($adminStart - $cell.Created).TotalMinutes
MinutesRemovalToAdmin = $(if ($hasPrevious) { '{0:N1}' -f ($adminStart - $previous.Removed).TotalMinutes })
T1ServerNameFileServer = $tests.T1
T2DefaultServerName = $tests.T2
T3UnreachableServerName = $tests.T3
EffectiveAccessFailures = $failures
})
}
$previous = [pscustomobject]@{ Removed = $cell.Removed; Subject = $cell.Subject; SubjectRid = $cell.SubjectRid }
}
$rows | Export-Csv -LiteralPath $OutputPath -NoTypeInformation -Encoding ASCII
'{0} rows for {1} cells written to {2}' -f $rows.Count, $cells.Count, $OutputPath

101
Tests/Lab/Acceptance/Export-MatrixResults.ps1

@ -0,0 +1,101 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $OutputPrefix,
[string] $MatrixRoot,
[string] $Label,
[string[]] $LocalSuiteFolder = @(),
[string] $ReferenceMachine = 'LOCAL'
)
# Turns the raw results of the operating-system matrix (Decision 24) into the tables of the acceptance record, in Windows PowerShell 5.1.
# -MatrixRoot and -Label name the sequences of Run-MatrixSequence.ps1 (folders <Label>-<file server>): per cell, edition, and role the
# counts, the skipped tests, and the operating systems of the readiness log. -LocalSuiteFolder lists the result folders of
# Run-MatrixLocalSuite.ps1 (folders <label>-<machine> with one JSON file per run): per machine, mode, and edition the counts, and the
# difference of the skipped tests to the reference machine (a skipped test that only one side skips is a difference). Every input file is
# listed with its SHA-256 in <OutputPrefix>-hashes.csv. Nothing is changed in the lab.
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$LocalSuiteFolder = @($LocalSuiteFolder | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$hashRows = New-Object -TypeName 'System.Collections.Generic.List[object]'
function Add-Hash { param ([string] $Path) $hashRows.Add([pscustomobject]@{ File = $Path; Sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash; Bytes = (Get-Item -LiteralPath $Path).Length }) }
function Get-Multiset { param ([string[]] $Name) $set = @{}; foreach ($item in @($Name | Where-Object -FilterScript { $_ })) { $set[$item] = 1 + [int] $set[$item] }; $set }
function Get-Excess {
param ([hashtable] $Left, [hashtable] $Right)
foreach ($key in ($Left.Keys | Sort-Object)) { $extra = [int] $Left[$key] - [int] $Right[$key]; if ($extra -gt 0) { '{0} (x{1})' -f $key, $extra } }
}
if ($MatrixRoot) {
$cellRows = New-Object -TypeName 'System.Collections.Generic.List[object]'
$skipRows = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ($folder in (Get-ChildItem -LiteralPath $MatrixRoot -Directory -Filter "$Label-*" | Sort-Object -Property Name)) {
$server = $folder.Name.Substring($Label.Length + 1)
$readiness = Join-Path -Path $folder.FullName -ChildPath 'readiness.log'
$operatingSystems = @{}
foreach ($match in (Select-String -LiteralPath $readiness -Pattern '^(\S+)\s+wsman=ok .* os=(.+?) type=(\d)')) {
$groups = $match.Matches[0].Groups
$operatingSystems[$groups[1].Value] = '{0} ({1})' -f ($groups[2].Value -replace '^Microsoft ', ''), $(if ($groups[3].Value -eq '1') { 'client' } else { 'server' })
}
$clientName = @($operatingSystems.Keys | Where-Object -FilterScript { $operatingSystems[$_] -like '*client*' })[0]
$counts = Import-Csv -LiteralPath (Join-Path -Path $folder.FullName -ChildPath "$Label-$server-counts.csv")
foreach ($row in $counts) {
$cellRows.Add([pscustomobject]@{
FileServer = $server; FileServerOs = $operatingSystems[$server]; Client = $clientName; ClientOs = $operatingSystems[$clientName]
Edition = $row.Edition; Role = $row.Role; Account = $row.Account; ExitCode = $row.ExitCode; Passed = $row.Passed; Failed = $row.Failed; Skipped = $row.Skipped
})
}
foreach ($test in (Import-Csv -LiteralPath (Join-Path -Path $folder.FullName -ChildPath "$Label-$server-tests.csv") | Where-Object -FilterScript { $_.Result -ne 'Passed' })) {
$skipRows.Add([pscustomobject]@{ FileServer = $server; Edition = $test.Edition; Role = $test.Role; Result = $test.Result; Test = $test.Test; Message = $test.Message })
}
foreach ($name in "$Label-$server-counts.csv", "$Label-$server-tests.csv", "$Label-$server-failures.csv", 'readiness.log', 'validation.log', 'run.log', 'fixture-sids.json', 'cleanup-1-snapshot.log', 'cleanup-2-remove.log', 'cleanup-3-verify.log') {
$path = Join-Path -Path $folder.FullName -ChildPath $name
if (Test-Path -LiteralPath $path) { Add-Hash -Path $path }
}
foreach ($summary in (Get-ChildItem -LiteralPath (Join-Path -Path $folder.FullName -ChildPath 'Results') -Filter 'Summary.json' -Recurse -ErrorAction SilentlyContinue)) { Add-Hash -Path $summary.FullName }
}
$cellRows | Export-Csv -LiteralPath "$OutputPrefix-cells.csv" -NoTypeInformation
$skipRows | Export-Csv -LiteralPath "$OutputPrefix-cells-skipped.csv" -NoTypeInformation
'{0} role rows and {1} tests that did not pass, in {2} cell(s)' -f $cellRows.Count, $skipRows.Count, @($cellRows | Select-Object -ExpandProperty FileServer -Unique).Count
}
if ($LocalSuiteFolder) {
$runs = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ($folder in $LocalSuiteFolder) {
$machine = ((Split-Path -Path $folder -Leaf) -split '-', 2)[1]
foreach ($json in (Get-ChildItem -LiteralPath $folder -Filter '*.json' | Sort-Object -Property Name)) {
$summary = Get-Content -LiteralPath $json.FullName -Raw | ConvertFrom-Json
Add-Hash -Path $json.FullName
$log = [IO.Path]::ChangeExtension($json.FullName, '.log')
if (Test-Path -LiteralPath $log) { Add-Hash -Path $log }
$runs.Add([pscustomobject]@{
Machine = $machine; Os = $summary.Os; Mode = $(if ($summary.Elevated) { 'Elevated' } else { 'Basic' }); Edition = $summary.Edition; PowerShell = $summary.PowerShell
Result = $summary.Result; Passed = $summary.Passed; Failed = $summary.Failed; Skipped = $summary.Skipped; Total = $summary.Total; Seconds = $summary.Seconds
SkippedTests = @($summary.SkippedTests | Where-Object -FilterScript { $_ }); FailedTests = @($summary.FailedTests | Where-Object -FilterScript { $_ })
})
}
}
$runs | Select-Object -Property Machine, Os, Mode, Edition, PowerShell, Result, Passed, Failed, Skipped, Total, Seconds | Sort-Object -Property Machine, Mode, Edition |
Export-Csv -LiteralPath "$OutputPrefix-localsuite.csv" -NoTypeInformation
$differences = foreach ($run in ($runs | Where-Object -FilterScript { $_.Machine -ne $ReferenceMachine })) {
$reference = $runs | Where-Object -FilterScript { $_.Machine -eq $ReferenceMachine -and $_.Mode -eq $run.Mode -and $_.Edition -eq $run.Edition } | Select-Object -First 1
if (-not $reference) { continue }
$mine = Get-Multiset -Name $run.SkippedTests
$theirs = Get-Multiset -Name $reference.SkippedTests
[pscustomobject]@{
Machine = $run.Machine; Mode = $run.Mode; Edition = $run.Edition; Skipped = $run.Skipped; ReferenceSkipped = $reference.Skipped
OnlyOnMachine = (@(Get-Excess -Left $mine -Right $theirs) -join ' | '); OnlyOnReference = (@(Get-Excess -Left $theirs -Right $mine) -join ' | ')
Failed = $run.Failed; FailedTests = ($run.FailedTests -join ' | ')
}
}
$differences | Sort-Object -Property Machine, Mode, Edition | Export-Csv -LiteralPath "$OutputPrefix-localsuite-skipdiff.csv" -NoTypeInformation
'{0} local-suite run(s) of {1} machine(s)' -f $runs.Count, @($runs | Select-Object -ExpandProperty Machine -Unique).Count
}
$hashRows | Export-Csv -LiteralPath "$OutputPrefix-hashes.csv" -NoTypeInformation
'{0} input file(s) hashed' -f $hashRows.Count

93
Tests/Lab/Acceptance/Invoke-EffectiveAccessProbe.ps1

@ -0,0 +1,93 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[Parameter(Mandatory)] [string] $Variant,
[string] $OtherServer = ''
)
# Runs inside a machine of the operating-system matrix, in Windows PowerShell 5.1 under the token that Probe-EffectiveAccess.ps1 chose for
# the variant, and asks Get-NTFSEffectiveAccess the same question in several ways: for the account of the token, Everyone, the local
# Administrator, and the domain Administrator and Domain Users on a computer in a domain, each for the default server name, localhost, an
# empty name, the names of this computer, and the computers of -OtherServer (a comma-separated list). For every call it writes the result,
# the number of warnings, and the native error with the failing method, so that the failing call of the authorization manager shows. It
# changes nothing but a folder below $env:TEMP.
$ErrorActionPreference = 'Continue'
$ProgressPreference = 'SilentlyContinue'
$stamp = '[{0:HH:mm:ss}]'
function Write-Probe { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $OutFile }
$null = New-Item -ItemType Directory -Path (Split-Path -Path $OutFile -Parent) -Force
Set-Content -LiteralPath $OutFile -Value ''
try {
Import-Module -Name (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.psd1') -Force -ErrorAction Stop
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $identity
$current = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
Write-Probe ('START variant={0} user={1} sid={2} administrator={3} os={4} {5}.{6} dll={7}' -f $Variant, $identity.Name, $identity.User.Value,
$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator), $current.ProductName, $current.CurrentBuildNumber, $current.UBR,
(Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.dll')).Hash.Substring(0, 12))
$groups = @(& whoami.exe /groups /fo csv | ConvertFrom-Csv)
Write-Probe ('groups={0}; deny only: {1}' -f $groups.Count, ((@($groups | Where-Object -FilterScript { $_.Attributes -match 'deny' } | ForEach-Object -Process { $_.'Group Name' })) -join ', '))
Write-Probe ('integrity: {0}' -f ((@($groups | Where-Object -FilterScript { $_.'Group Name' -like 'Mandatory Label*' } | ForEach-Object -Process { $_.'Group Name' })) -join ', '))
$privileges = @(& whoami.exe /priv /fo csv | ConvertFrom-Csv)
Write-Probe ('privileges present={0} enabled: {1}' -f $privileges.Count, ((@($privileges | Where-Object -FilterScript { $_.State -eq 'Enabled' } | ForEach-Object -Process { $_.'Privilege Name' })) -join ', '))
$folder = Join-Path -Path $env:TEMP -ChildPath ('probe-{0}' -f [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $folder
$fqdn = try { [Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName } catch { $env:COMPUTERNAME }
function Resolve-Sid {
param ([string] $Name)
try { (New-Object -TypeName 'Security.Principal.NTAccount' -ArgumentList $Name).Translate([Security.Principal.SecurityIdentifier]).Value } catch { '' }
}
$computer = Get-CimInstance -ClassName Win32_ComputerSystem
Write-Probe ('computer {0} domain joined={1} domain={2}' -f $env:COMPUTERNAME, $computer.PartOfDomain, $computer.Domain)
$accounts = [ordered]@{ 'self' = ''; 'Everyone' = 'S-1-1-0'; 'local Administrator' = (Resolve-Sid -Name ('{0}\Administrator' -f $env:COMPUTERNAME)) }
if ($computer.PartOfDomain) {
$accounts['domain Administrator'] = Resolve-Sid -Name ('{0}\Administrator' -f $computer.Domain)
$accounts['Domain Users'] = Resolve-Sid -Name ('{0}\Domain Users' -f $computer.Domain)
}
$servers = [ordered]@{ 'no -ServerName' = $null; 'localhost' = 'localhost'; 'empty name' = ''; 'computer name' = $env:COMPUTERNAME; 'fqdn' = $fqdn }
foreach ($name in @($OtherServer -split ',' | Where-Object -FilterScript { $_ })) { $servers["other computer $name"] = $name }
$cases = foreach ($accountName in $accounts.Keys) {
if ($accountName -ne 'self' -and -not $accounts[$accountName]) { continue }
foreach ($serverName in $servers.Keys) {
$arguments = @{}
if ($accounts[$accountName]) { $arguments.Account = $accounts[$accountName] }
if ($null -ne $servers[$serverName]) { $arguments.ServerName = $servers[$serverName] }
@{ Name = ('{0}, {1}' -f $accountName, $serverName); Arguments = $arguments }
}
}
foreach ($case in $cases) {
$arguments = $case.Arguments
$errorList = $null
$warningList = $null
try {
$result = @(Get-NTFSEffectiveAccess -Path $folder @arguments -ErrorVariable errorList -WarningVariable warningList -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)
}
catch {
$result = @()
$errorList = @($_)
}
$access = if ($result.Count -gt 0) { ('{0}' -f $result[0].AccessRights) } else { 'none' }
$warnings = @($warningList | ForEach-Object -Process { ('{0}' -f $_.Message) -replace '\s+', ' ' } | ForEach-Object -Process { if ($_.Length -gt 60) { $_.Substring(0, 60) } else { $_ } })
Write-Probe ('CASE {0}: results={1} access={2} errors={3} warnings={4}' -f $case.Name, $result.Count, $access, @($errorList).Count, $warnings.Count)
foreach ($record in @($errorList)) {
$inner = $record.Exception
while ($inner.InnerException) { $inner = $inner.InnerException }
$native = if ($inner -is [ComponentModel.Win32Exception]) { $inner.NativeErrorCode } else { '' }
$frames = (('{0}' -f $inner.StackTrace) -split "`r?`n" | Select-Object -First 1 | ForEach-Object -Process { $_.Trim() -replace '^at ', '' -replace '\(.*$', '' }) -join ' <- '
Write-Probe (' ERROR id={0} type={1} native={2} message={3} frames={4}' -f $record.FullyQualifiedErrorId, $inner.GetType().Name, $native, $inner.Message, $frames)
}
}
Remove-Item -LiteralPath $folder -Recurse -Force -ErrorAction SilentlyContinue
Write-Probe 'DONE'
}
catch {
Write-Probe ('FAILED: {0}' -f $_)
}

66
Tests/Lab/Acceptance/Invoke-LocalSuite.ps1

@ -0,0 +1,66 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^[\w-]+$')] [string] $Label,
[Parameter(Mandatory)] [string] $Root,
[Parameter(Mandatory)] [string] $OutDir,
[string] $PesterVersion = '5.7.1',
[string] $PesterModulePath
)
# Runs the Pester files of <Root>\Tests in this process, Windows PowerShell 5.1 or PowerShell 7, against the module in
# <Root>\NTFSSecurity\bin\Release, like .github\scripts\Invoke-Tests.ps1 does for the repository. It writes the log, the NUnit result, a JSON
# summary, and an exit code file to <OutDir>. Run it in a new process for every edition. The tests keep to their own sandbox folders
# below $env:TEMP.
$ErrorActionPreference = 'Stop'
$null = New-Item -ItemType Directory -Path $OutDir -Force
$log = Join-Path -Path $OutDir -ChildPath "$Label.log"
$script:exitCode = 1
if ($PSVersionTable.PSEdition -eq 'Desktop') {
# A Windows PowerShell process started by PowerShell 7 would inherit the module path of PowerShell 7.
$env:PSModulePath = @(
(Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules'),
(Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules'),
(Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\Modules')
) -join ';'
}
& {
$principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent())
$elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$current = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
'[{0:yyyy-MM-dd HH:mm:ss}Z] START {1} edition={2} {3} elevated={4} os={5} build={6}.{7} user={8}' -f [DateTime]::UtcNow, $Label, $PSVersionTable.PSEdition,
$PSVersionTable.PSVersion, $elevated, $current.ProductName, $current.CurrentBuildNumber, $current.UBR, [Security.Principal.WindowsIdentity]::GetCurrent().Name
try {
$watch = [Diagnostics.Stopwatch]::StartNew()
if ($PesterModulePath) { Import-Module -Name (Join-Path -Path $PesterModulePath -ChildPath 'Pester.psd1') -Force -ErrorAction Stop }
else { Import-Module -Name Pester -RequiredVersion $PesterVersion -Force -ErrorAction Stop }
$configuration = New-PesterConfiguration
$configuration.Run.Path = @(Join-Path -Path $Root -ChildPath 'Tests')
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Normal'
# The NUnit file is written below, after the summary: its writer asks WMI for the environment, which a restricted token may not do.
$configuration.TestResult.Enabled = $false
$result = Invoke-Pester -Configuration $configuration
'RESULT result={0} passed={1} failed={2} skipped={3} notrun={4} total={5} failedContainers={6} seconds={7:N0}' -f $result.Result, $result.PassedCount,
$result.FailedCount, $result.SkippedCount, $result.NotRunCount, $result.TotalCount, $result.FailedContainersCount, $watch.Elapsed.TotalSeconds
foreach ($test in $result.Failed) { 'FAILED: {0}: {1}' -f $test.ExpandedPath, ("$(@($test.ErrorRecord)[0])" -replace '\s+', ' ') }
foreach ($test in $result.Skipped) { 'SKIPPED: {0}' -f $test.ExpandedPath }
[pscustomobject]@{
Label = $Label; Edition = $PSVersionTable.PSEdition; PowerShell = $PSVersionTable.PSVersion.ToString(); Elevated = $elevated
Os = '{0} {1}.{2}' -f $current.ProductName, $current.CurrentBuildNumber, $current.UBR; Result = [string] $result.Result
Passed = $result.PassedCount; Failed = $result.FailedCount; Skipped = $result.SkippedCount; NotRun = $result.NotRunCount; Total = $result.TotalCount
FailedContainers = $result.FailedContainersCount; Seconds = [int] $watch.Elapsed.TotalSeconds
FailedTests = @($result.Failed | ForEach-Object -Process { $_.ExpandedPath }); SkippedTests = @($result.Skipped | ForEach-Object -Process { $_.ExpandedPath })
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path -Path $OutDir -ChildPath "$Label.json") -Encoding UTF8
try { Export-NUnitReport -Result $result -Path (Join-Path -Path $OutDir -ChildPath "$Label.xml") }
catch { 'NUnit report not written: {0}' -f $_.Exception.Message }
if ($result.Result -eq 'Passed') { $script:exitCode = 0 }
'[{0:yyyy-MM-dd HH:mm:ss}Z] {1}-DONE' -f [DateTime]::UtcNow, $Label
}
catch {
'ERROR: {0}' -f $_
'[{0:yyyy-MM-dd HH:mm:ss}Z] {1}-FAILED' -f [DateTime]::UtcNow, $Label
}
} *>&1 | Out-File -FilePath $log -Encoding utf8 -Width 400
Set-Content -LiteralPath (Join-Path -Path $OutDir -ChildPath "$Label.exit") -Value $script:exitCode
exit $script:exitCode

151
Tests/Lab/Acceptance/Probe-AccountRecreation.ps1

@ -0,0 +1,151 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string[]] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[string] $Client = 'OSWin11E',
[string] $DomainController = 'OSDC1',
[string] $FileServer = 'OSFile22',
[ValidateRange(2, 20)] [int] $Rounds = 4,
[string] $LabName = 'NtfsSecurityOsMatrixLab'
)
# Probe of the groups that a computer reports for an account that was deleted and created again with the same name (Decision 24). In each
# round it creates a user in a group that is in another group, with the same names and new SIDs, and a folder on the file server whose DACL
# grants the outer group ReadAndExecute. Then it logs the user on with Kerberos S4U, like the oracle of the live tests does, on the domain
# controller, the client, and the file server, and asks from the client, in a new process for each module, for the effective access of the
# account on the folder by name and by SID, with the default server name and with the name of the file server. -ModulePath takes module
# folders as label=path, such as baseline=C:\Build\NTFSSecurity. From the second round on, a computer that still holds the deleted account
# reports its SID, and every module reports no access (Synchronize only), whichever its version. The probe removes everything it created; the
# accounts, the folder, and the files on the client are named NtfsProbe*, so Test-MatrixCleanup.ps1 reports a leftover. The password of the
# user is random and exists only in memory. Windows PowerShell 5.1 on the Hyper-V host, with AutomatedLab.
& {
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$modules = @(
foreach ($entry in @($ModulePath | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })) {
$label, $path = $entry -split '=', 2
if (-not $path -or -not (Test-Path -LiteralPath (Join-Path -Path $path -ChildPath 'NTFSSecurity.psd1'))) { throw "-ModulePath takes label=folder with a module; '$entry' has none." }
[pscustomobject]@{ Label = $label; Path = $path }
}
)
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
$domainName = (Get-Lab).Domains[0].Name
$netBiosName = $domainName.Split('.')[0].ToUpperInvariant()
$dcSession = New-LabPSSession -ComputerName $DomainController
$clientSession = New-LabPSSession -ComputerName $Client
$serverSession = New-LabPSSession -ComputerName $FileServer
$machines = [ordered]@{ $DomainController = $dcSession; $Client = $clientSession; $FileServer = $serverSession }
$userName = 'NtfsProbeSubject'
$innerName = 'NtfsProbeInner'
$outerName = 'NtfsProbeOuter'
$folderName = 'NtfsProbeRecreation'
$stageName = 'C:\NtfsProbeModules'
$report = New-Object -TypeName 'System.Collections.Generic.List[string]'
$bytes = New-Object -TypeName 'byte[]' -ArgumentList 24
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
try { $generator.GetBytes($bytes) } finally { $generator.Dispose() }
# Base64 has upper case and lower case letters and digits; the suffix adds the other classes of a domain's complexity rules.
$secret = New-Object -TypeName 'System.Security.SecureString'
foreach ($character in ([Convert]::ToBase64String($bytes) + '!a1Z').ToCharArray()) { $secret.AppendChar($character) }
$secret.MakeReadOnly()
$removeOnDcScript = {
param ($User, $Inner, $Outer)
Import-Module -Name ActiveDirectory
foreach ($name in $User) { Get-ADUser -Filter "SamAccountName -eq '$name'" | Remove-ADUser -Confirm:$false }
foreach ($name in $Inner, $Outer) { Get-ADGroup -Filter "SamAccountName -eq '$name'" | Remove-ADGroup -Confirm:$false }
}
$createOnDcScript = {
param ($User, $Inner, $Outer, [securestring] $Secret)
Import-Module -Name ActiveDirectory
$null = New-ADGroup -Name $Outer -SamAccountName $Outer -GroupScope Global
$null = New-ADGroup -Name $Inner -SamAccountName $Inner -GroupScope Global
Add-ADGroupMember -Identity $Outer -Members $Inner
$null = New-ADUser -Name $User -SamAccountName $User -UserPrincipalName ('{0}@{1}' -f $User, (Get-ADDomain).DNSRoot) -AccountPassword $Secret -Enabled $true
Add-ADGroupMember -Identity $Inner -Members $User
[pscustomobject]@{ User = (Get-ADUser -Identity $User).SID.Value; Outer = (Get-ADGroup -Identity $Outer).SID.Value }
}
$setFolderScript = {
param ($Folder, $OuterSid)
$path = Join-Path -Path $env:SystemDrive -ChildPath $Folder
if (-not (Test-Path -LiteralPath $path)) { $null = New-Item -ItemType Directory -Path $path }
$acl = New-Object -TypeName 'System.Security.AccessControl.DirectorySecurity'
$acl.SetAccessRuleProtection($true, $false)
foreach ($entry in @(@('S-1-5-32-544', 'FullControl'), @('S-1-5-18', 'FullControl'), @($OuterSid, 'ReadAndExecute'))) {
$acl.AddAccessRule((New-Object -TypeName 'System.Security.AccessControl.FileSystemAccessRule' -ArgumentList ([Security.Principal.SecurityIdentifier] $entry[0]), $entry[1], 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
}
Set-Acl -LiteralPath $path -AclObject $acl
}
$tokenScript = {
param ($User, $Domain, $UserSid, $OuterSid)
try {
$identity = New-Object -TypeName 'System.Security.Principal.WindowsIdentity' -ArgumentList ('{0}@{1}' -f $User, $Domain)
$groups = @($identity.Groups | ForEach-Object -Process { $_.Value })
'S4U token of the {0} account, outer group {1}' -f $(if ($identity.User.Value -eq $UserSid) { 'CURRENT' } else { 'OLD' }), ($groups -contains $OuterSid)
}
catch { 'S4U logon failed: ' + $_.Exception.Message }
}
$effectiveScript = {
param ($ModuleFolder, $Folder, $Server, $Domain, $NetBios, $User, $UserSid)
Import-Module -Name (Join-Path -Path $ModuleFolder -ChildPath 'NTFSSecurity.psd1') -Force
$unc = '\\{0}.{1}\C$\{2}' -f $Server, $Domain, $Folder
$name = '{0}\{1}' -f $NetBios, $User
function Measure-Answer {
param ([hashtable] $Arguments)
$result = @(Get-NTFSEffectiveAccess @Arguments -WarningAction SilentlyContinue -ErrorAction SilentlyContinue -ErrorVariable failures)
$value = if ($result.Count) { '0x{0:X}' -f ([long] $result[0].AccessRights) } else { 'none' }
if (@($failures).Count) { $value += ' ERR ' + $failures[0].Exception.Message }
$value
}
$serverName = '{0}.{1}' -f $Server, $Domain
'effective access by name {0}, by name and server {1}, by SID {2}, by SID and server {3}' -f
(Measure-Answer -Arguments @{ Path = $unc; Account = $name }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $name; ServerName = $serverName }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $UserSid }),
(Measure-Answer -Arguments @{ Path = $unc; Account = $UserSid; ServerName = $serverName })
}
$runEffectiveScript = {
param ($Stage, $ModuleLabel, $ScriptText, $Folder, $Server, $Domain, $NetBios, $User, $UserSid)
$block = [scriptblock]::Create($ScriptText)
$powershell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
& $powershell -NoProfile -ExecutionPolicy Bypass -Command $block -args (Join-Path -Path $Stage -ChildPath $ModuleLabel), $Folder, $Server, $Domain, $NetBios, $User, $UserSid
}
try {
Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName
Invoke-Command -Session $clientSession -ArgumentList $stageName -ScriptBlock { param ($Stage) if (Test-Path -LiteralPath $Stage) { Remove-Item -LiteralPath $Stage -Recurse -Force }; $null = New-Item -ItemType Directory -Path $Stage -Force }
foreach ($module in $modules) {
Invoke-Command -Session $clientSession -ArgumentList (Join-Path -Path $stageName -ChildPath $module.Label) -ScriptBlock { param ($Path) $null = New-Item -ItemType Directory -Path $Path -Force }
Copy-Item -Path (Join-Path -Path $module.Path -ChildPath '*') -Destination (Join-Path -Path $stageName -ChildPath $module.Label) -ToSession $clientSession -Recurse -Force
}
for ($round = 1; $round -le $Rounds; $round++) {
$created = Invoke-Command -Session $dcSession -ScriptBlock $createOnDcScript -ArgumentList $userName, $innerName, $outerName, $secret
Invoke-Command -Session $serverSession -ScriptBlock $setFolderScript -ArgumentList $folderName, $created.Outer
$report.Add(('round {0} at {1:HH:mm:ss}Z: subject {2}, outer group {3}' -f $round, [DateTime]::UtcNow, $created.User, $created.Outer))
foreach ($machine in $machines.Keys) {
$report.Add((' {0}: {1}' -f $machine, (Invoke-Command -Session $machines[$machine] -ScriptBlock $tokenScript -ArgumentList $userName, $domainName, $created.User, $created.Outer)))
}
# The order of the modules alternates, so that the module that asks first is not always the same.
$ordered = if ($round % 2) { $modules } else { @($modules)[($modules.Count - 1)..0] }
foreach ($module in $ordered) {
$answer = Invoke-Command -Session $clientSession -ArgumentList $stageName, $module.Label, $effectiveScript.ToString(), $folderName, $FileServer, $domainName, $netBiosName, $userName, $created.User -ScriptBlock $runEffectiveScript
$report.Add((' {0} on {1}: {2}' -f $module.Label, $Client, (@($answer) -join ' ')))
}
Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName
}
}
finally {
try { Invoke-Command -Session $dcSession -ScriptBlock $removeOnDcScript -ArgumentList $userName, $innerName, $outerName } catch { $report.Add("cleanup on the domain controller failed: $($_.Exception.Message)") }
try { Invoke-Command -Session $serverSession -ArgumentList $folderName -ScriptBlock { param ($Folder) Remove-Item -LiteralPath (Join-Path -Path $env:SystemDrive -ChildPath $Folder) -Recurse -Force -ErrorAction SilentlyContinue } } catch { $report.Add("cleanup on the file server failed: $($_.Exception.Message)") }
try { Invoke-Command -Session $clientSession -ArgumentList $stageName -ScriptBlock { param ($Stage) Remove-Item -LiteralPath $Stage -Recurse -Force -ErrorAction SilentlyContinue } } catch { $report.Add("cleanup on the client failed: $($_.Exception.Message)") }
$report | Set-Content -LiteralPath $OutFile -Encoding utf8
Remove-PSSession -Session @($machines.Values) -ErrorAction SilentlyContinue
}
'done'
}

278
Tests/Lab/Acceptance/Probe-EffectiveAccess.ps1

@ -0,0 +1,278 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The lab installation password comes from the AutomatedLab lab definition, which stores it as text, and the passwords of the probe users are random and exist only in memory; no credential is written.'
)]
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^[\w-]+$')] [string] $Label,
[Parameter(Mandatory)] [string] $Machine,
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutputRoot,
[string] $Variant = 'Elevated,Safer,Standard',
[string] $OtherServer = '',
[string] $DomainController = 'OSDC1',
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string] $LocalCredentialMachine = '',
[string] $RepositoryRoot,
[ValidateRange(1, 60)] [int] $TimeoutMinutes = 10
)
# Diagnostic of the operating-system matrix (Decision 24), in Windows PowerShell 5.1 on the Hyper-V host: runs Invoke-EffectiveAccessProbe.ps1
# on one machine under up to four tokens, one after the other, and copies the output back.
# Elevated the lab account in a scheduled task at the highest run level (the elevated mode of the local suite)
# Limited the same account at the limited run level; a task with a batch logon doesn't get a filtered token, so this repeats Elevated
# Safer the token of a basic user that Run-MatrixLocalSuite.ps1 -Mode Basic uses (SAFER level Normal User)
# Standard a local standard user that this script creates on the machine and removes again, with a password that only exists there
# DomainStandard a standard user of the domain, created on the domain controller (-DomainController) and removed again
# The standard users get the batch logon right through the group Performance Log Users, which has no other right that the check needs.
# Nothing secret is written; the lab password stays in memory, as in Run-MatrixLocalSuite.ps1.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 leaves $PSScriptRoot empty in a parameter default when the script runs with -File.
if (-not $RepositoryRoot) { $RepositoryRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent }
$variants = @($Variant -split ',' | Where-Object -FilterScript { $_ })
if ($variants | Where-Object -FilterScript { $_ -notin 'Elevated', 'Limited', 'Safer', 'Standard', 'DomainStandard' }) { throw '-Variant takes Elevated, Limited, Safer, Standard, and DomainStandard, separated by commas.' }
$localCredential = @($LocalCredentialMachine -split ',' | Where-Object -FilterScript { $_ })
$cellFolder = Join-Path -Path $OutputRoot -ChildPath "$Label-$Machine"
$null = New-Item -ItemType Directory -Path $cellFolder -Force
$log = Join-Path -Path $cellFolder -ChildPath "$Label-probe.log"
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $log }
Set-Content -LiteralPath $log -Value (($stamp -f [DateTime]::UtcNow) + " START probe-$Label machine=$Machine variants=$($variants -join ',')")
$wrapper = Get-Content -LiteralPath (Join-Path -Path $RepositoryRoot -ChildPath '.github\scripts\Invoke-TestsAsBasicUser.ps1') -Raw
$class = [regex]::Match($wrapper, "(?s)Add-Type -TypeDefinition @'\r?\n(.*?)\r?\n'@").Groups[1].Value
if (-not $class) { throw 'The class of the basic-user wrapper was not found in .github\scripts\Invoke-TestsAsBasicUser.ps1.' }
$saferHead = @'
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $Executable,
[Parameter(Mandatory)] [string] $Arguments,
[Parameter(Mandatory)] [string] $WorkDirectory,
[Parameter(Mandatory)] [string] $Console
)
# Generated by Probe-EffectiveAccess.ps1: starts a process with the token of a basic user (SAFER level Normal User) through the class of
# .github\scripts\Invoke-TestsAsBasicUser.ps1 and waits for it.
$ErrorActionPreference = 'Stop'
'@
$saferTail = @'
$commandLine = 'cmd.exe /d /s /c ""{0}" {1} > "{2}" 2>&1"' -f $Executable, $Arguments, $Console
exit [NTFSSecurityBasicUserProcess]::Run((Join-Path -Path $env:SystemRoot -ChildPath 'System32\cmd.exe'), $commandLine, $WorkDirectory)
'@
$stage = Join-Path -Path $env:TEMP -ChildPath "ntfs-probe-$Label"
if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
$null = New-Item -ItemType Directory -Path (Join-Path -Path $stage -ChildPath 'module') -Force
Copy-Item -Path (Join-Path -Path $ModulePath -ChildPath '*') -Destination (Join-Path -Path $stage -ChildPath 'module') -Recurse
Copy-Item -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath 'Invoke-EffectiveAccessProbe.ps1') -Destination $stage
Set-Content -LiteralPath (Join-Path -Path $stage -ChildPath 'Start-SaferProcess.ps1') -Encoding UTF8 -Value ($saferHead + "`r`nAdd-Type -TypeDefinition @'`r`n" + $class + "`r`n'@`r`n" + $saferTail)
$dllHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.dll')).Hash
Write-Step "module dll=$dllHash"
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
$sessionParameters = @{ ComputerName = $Machine }
if ($Machine -in $localCredential) { $sessionParameters.UseLocalCredential = $true }
$session = New-LabPSSession @sessionParameters
$machineDefinition = Get-LabVM -ComputerName $Machine
$runCredential = if ($Machine -in $localCredential) {
New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ('{0}\{1}' -f $Machine, $machineDefinition.InstallationUser.UserName), (ConvertTo-SecureString -String $machineDefinition.InstallationUser.Password -AsPlainText -Force)
}
else {
$machineDefinition.GetCredential((Get-Lab))
}
$root = 'C:\NtfsMatrixProbe\' + $Label
# A new name for every run: Windows keeps the SID of a deleted account for its name for a while, and a profile that stays loaded keeps the
# folder, so a name that is used again meets the leftovers of its predecessor.
$suffix = [DateTime]::UtcNow.ToString('MMddHHmmss')
$standardUser = 'NtfsProbeS' + $suffix
$domainUser = 'NtfsProbeD' + $suffix
$dcSession = $null
$domainSid = ''
function Get-RandomProbePassword {
# Random and never written; it exists in memory and in the account that the probe removes.
$bytes = New-Object -TypeName 'byte[]' -ArgumentList 24
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$alphabet = [char[]] 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
'Aa1!' + (-join ($bytes | ForEach-Object -Process { $alphabet[$_ % $alphabet.Length] }))
}
try {
Invoke-Command -Session $session -ArgumentList $root -ScriptBlock {
param ($Path)
if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Recurse -Force }
$null = New-Item -ItemType Directory -Path (Join-Path -Path $Path -ChildPath 'out') -Force
}
Copy-Item -Path (Join-Path -Path $stage -ChildPath '*') -Destination $root -ToSession $session -Recurse -Force
Write-Step "staged to $root"
$start = {
param ($Root, $Variant, $UserName, $Password, $OtherServer, $StandardUser, $DomainSid)
$powershell = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
$out = Join-Path -Path $Root -ChildPath 'out'
$outFile = Join-Path -Path $out -ChildPath ('{0}.txt' -f $Variant)
$probe = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -ModulePath "{1}" -OutFile "{2}" -Variant {3}' -f (Join-Path -Path $Root -ChildPath 'Invoke-EffectiveAccessProbe.ps1'),
(Join-Path -Path $Root -ChildPath 'module'), $outFile, $Variant
if ($OtherServer) { $probe += ' -OtherServer "{0}"' -f $OtherServer }
$taskName = 'NtfsMatrixProbe-' + $Variant
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$runLevel = 'Highest'
if ($Variant -eq 'Standard') {
# The password exists only here: random, never written, and the account is removed after the run.
$bytes = New-Object -TypeName 'byte[]' -ArgumentList 24
$generator = [Security.Cryptography.RandomNumberGenerator]::Create()
$generator.GetBytes($bytes)
$alphabet = [char[]] 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
$Password = 'Aa1!' + (-join ($bytes | ForEach-Object -Process { $alphabet[$_ % $alphabet.Length] }))
$UserName = '{0}\{1}' -f $env:COMPUTERNAME, $StandardUser
if (Get-LocalUser -Name $StandardUser -ErrorAction SilentlyContinue) { Remove-LocalUser -Name $StandardUser }
$null = New-LocalUser -Name $StandardUser -Password (ConvertTo-SecureString -String $Password -AsPlainText -Force) -PasswordNeverExpires -UserMayNotChangePassword -Description 'Probe of the matrix, removed after the run'
Add-LocalGroupMember -SID 'S-1-5-32-559' -Member $StandardUser
$null = & icacls.exe $out /grant ('{0}:(OI)(CI)M' -f $StandardUser)
$runLevel = 'Limited'
$execute = $powershell
$argument = $probe
}
elseif ($Variant -eq 'DomainStandard') {
# By SID: a name of a deleted account of an earlier run can still resolve to its old SID.
try { Add-LocalGroupMember -SID 'S-1-5-32-559' -Member $DomainSid }
catch { if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw } }
$null = & icacls.exe $out /grant ('{0}:(OI)(CI)M' -f $UserName)
$runLevel = 'Limited'
$execute = $powershell
$argument = $probe
}
elseif ($Variant -eq 'Limited') {
$runLevel = 'Limited'
$execute = $powershell
$argument = $probe
}
elseif ($Variant -eq 'Safer') {
$null = & icacls.exe $out /grant ('{0}:(OI)(CI)M' -f $UserName)
$execute = $powershell
$argument = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -Executable "{1}" -Arguments "{2}" -WorkDirectory "{3}" -Console "{4}"' -f (Join-Path -Path $Root -ChildPath 'Start-SaferProcess.ps1'),
$powershell, ($probe -replace '"', '\"'), $Root, (Join-Path -Path $out -ChildPath 'safer.console.txt')
}
else {
$execute = $powershell
$argument = $probe
}
$action = New-ScheduledTaskAction -Execute $execute -Argument $argument
$null = Register-ScheduledTask -TaskName $taskName -Action $action -RunLevel $runLevel -User $UserName -Password $Password
Start-ScheduledTask -TaskName $taskName
$taskName
}
$isRunning = {
param ($TaskName)
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
[bool] ($task -and $task.State -eq 'Running')
}
$finish = {
param ($TaskName)
$result = (Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue).LastTaskResult
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
"task result $result"
}
foreach ($name in $variants) {
$password = if ($name -in 'Elevated', 'Limited', 'Safer') { $runCredential.GetNetworkCredential().Password } else { '' }
$userName = if ($name -in 'Elevated', 'Limited', 'Safer') { $runCredential.UserName } else { '' }
if ($name -eq 'DomainStandard') {
if (-not $dcSession) { $dcSession = New-LabPSSession -ComputerName $DomainController }
$password = Get-RandomProbePassword
$domainSid = Invoke-Command -Session $dcSession -ArgumentList $domainUser, $password -ScriptBlock {
param ($Name, $Secret)
Import-Module -Name ActiveDirectory
New-ADUser -Name $Name -SamAccountName $Name -AccountPassword (ConvertTo-SecureString -String $Secret -AsPlainText -Force) -Enabled $true -PasswordNeverExpires $true -CannotChangePassword $true -Description 'Probe of the matrix, removed after the run'
(Get-ADUser -Identity $Name).SID.Value
}
$userName = '{0}\{1}' -f ((Get-Lab).Domains[0].Name -split '\.')[0], $domainUser
Write-Step "domain user $domainUser created ($domainSid)"
}
$taskName = Invoke-Command -Session $session -ScriptBlock $start -ArgumentList $root, $name, $userName, $password, $OtherServer, $standardUser, $domainSid
Write-Step "variant $name started ($taskName)"
$deadline = [DateTime]::UtcNow.AddMinutes($TimeoutMinutes)
do {
Start-Sleep -Seconds 5
$alive = Invoke-Command -Session $session -ScriptBlock $isRunning -ArgumentList $taskName
} while ($alive -and [DateTime]::UtcNow -lt $deadline)
if ($alive) { Write-Step "variant $name TIMED OUT after $TimeoutMinutes minutes" }
Write-Step ("variant $name finished: " + (Invoke-Command -Session $session -ScriptBlock $finish -ArgumentList $taskName))
}
Copy-Item -FromSession $session -Path (Join-Path -Path $root -ChildPath 'out\*') -Destination $cellFolder -Recurse -Force
Write-Step 'results copied back'
}
finally {
# The domain account goes first: its member entry on the machine is then an orphaned SID, which the cleanup of the machine removes.
if ($dcSession) {
# A failure here must not skip the cleanup of the machine below.
try {
$dcLeftOver = Invoke-Command -Session $dcSession -ArgumentList $domainUser -ScriptBlock {
param ($Name)
Import-Module -Name ActiveDirectory
if (Get-ADUser -Filter "SamAccountName -eq '$Name'") { Remove-ADUser -Identity $Name -Confirm:$false }
if (Get-ADUser -Filter "SamAccountName -eq '$Name'") { "domain user $Name still exists" }
}
Write-Step ('cleanup of the domain controller: ' + $(if (@($dcLeftOver).Count -eq 0) { 'nothing left' } else { @($dcLeftOver) -join '; ' }))
}
catch {
Write-Step ("cleanup of the domain controller FAILED, remove the domain user $domainUser by hand: " + $_.Exception.Message)
}
Remove-PSSession -Session $dcSession -ErrorAction SilentlyContinue
}
if ($session) {
# The accounts and the files of the probe don't stay on the machine. The cleanup finds them by name and by orphaned SID, not by
# what this run created, so it also repairs what a run that stopped early left.
$leftOver = Invoke-Command -Session $session -ArgumentList $root, $standardUser, $domainSid -ScriptBlock {
param ($Root, $StandardUser, $DomainSid)
$report = New-Object -TypeName 'System.Collections.Generic.List[string]'
$users = Join-Path -Path $env:SystemDrive -ChildPath 'Users'
function Get-ProbeProfile { @(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.LocalPath -like (Join-Path -Path $users -ChildPath 'NtfsProbe*') }) }
function Get-ProbeMember {
# net.exe shows the member of a deleted account as its SID.
foreach ($line in @(cmd.exe /d /c 'net localgroup "Performance Log Users" 2>&1')) {
$member = ('{0}' -f $line).Trim()
if ($member -match '^S-1-5-21-[\d-]+$' -or $member -match '\\NtfsProbe') { $member }
}
}
@(Get-ScheduledTask -TaskName 'NtfsMatrixProbe-*' -ErrorAction SilentlyContinue) | ForEach-Object -Process { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -ErrorAction SilentlyContinue }
if (Get-LocalUser -Name $StandardUser -ErrorAction SilentlyContinue) { Remove-LocalUser -Name $StandardUser }
# net.exe doesn't take the SID of an account that its name cache still resolves, so the member goes by its SID through the cmdlet.
if ($DomainSid) {
try { Remove-LocalGroupMember -SID 'S-1-5-32-559' -Member $DomainSid -ErrorAction Stop }
catch { if ("$($_.Exception.Message)" -notlike '*was not found*') { $report.Add("member ${DomainSid}: $($_.Exception.Message)") } }
}
# A profile that the last task of the account used stays loaded for a few seconds, so the removal is repeated.
$attempt = 0
do {
$profiles = Get-ProbeProfile
if ($profiles.Count -gt 0) {
$profiles | Remove-CimInstance -ErrorAction SilentlyContinue
if ((Get-ProbeProfile).Count -gt 0) { Start-Sleep -Seconds 3 }
}
$attempt++
} while ((Get-ProbeProfile).Count -gt 0 -and $attempt -lt 10)
Get-ChildItem -LiteralPath $users -Filter 'NtfsProbe*' -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $Root) { Remove-Item -LiteralPath $Root -Recurse -Force -ErrorAction SilentlyContinue }
if (Get-LocalUser -Name $StandardUser -ErrorAction SilentlyContinue) { $report.Add("user $StandardUser still exists") }
foreach ($member in @(Get-ProbeMember)) { $report.Add("$member is still in Performance Log Users") }
foreach ($folder in @(Get-ChildItem -LiteralPath $users -Filter 'NtfsProbe*' -Force -ErrorAction SilentlyContinue)) { $report.Add("profile folder $($folder.Name) still exists") }
foreach ($userProfile in (Get-ProbeProfile)) { $report.Add("profile $($userProfile.LocalPath) still exists") }
if (Test-Path -LiteralPath $Root) { $report.Add("folder $Root still exists") }
@(Get-ScheduledTask -TaskName 'NtfsMatrixProbe-*' -ErrorAction SilentlyContinue) | ForEach-Object -Process { $report.Add("task $($_.TaskName) still exists") }
$report
}
Write-Step ('cleanup: ' + $(if (@($leftOver).Count -eq 0) { 'nothing left on the machine' } else { @($leftOver) -join '; ' }))
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}
}
Write-Step "probe-$Label-DONE"

224
Tests/Lab/Acceptance/Probe-LaterCommand.ps1

@ -0,0 +1,224 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Pester passes the data to the blocks of the container.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'The tests read the variables that BeforeAll sets.')]
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[string] $PesterPath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1'
)
# Diagnostic of the acceptance in Acceptance-2026-10-09-quality-gate-paths.md, not part of it: why do the Select-Object rows of case 10
# fail on the base of the branch without a message? It runs the bodies of those tests (Assert-LabPipelineStop and
# Assert-LabDownstreamFailure of NTFSSecurity.Live.Tests.ps1) against files in a new folder below TEMP, with the settings of the
# runner (Pester 5.7.1, ErrorActionPreference Stop, detailed plain text), and lists for each test its result and error records, and
# the state of the items afterwards. One module build in one edition per process, never imported into another session; the script
# removes its own folder at the end after it has checked the path. Windows only. For example:
# powershell.exe -NoProfile -File Probe-LaterCommand.ps1 -ModulePath <folder with NTFSSecurity.psd1> -OutFile <result.txt>
$ErrorActionPreference = 'Stop'
Import-Module -Name (Join-Path -Path $PesterPath -ChildPath 'Pester.psd1') -Force
$root = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('mute-probe-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $root
$account = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$lines = New-Object -TypeName 'System.Collections.Generic.List[string]'
try {
$container = New-PesterContainer -ScriptBlock {
param ($ModulePath, $Root, $Account)
BeforeAll {
Import-Module -Name (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.psd1') -Force -ErrorAction Stop
$everyone = 'S-1-1-0'
$administrators = 'S-1-5-32-544'
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
$privateData['EnablePrivileges'] = $false
$account = $Account
function Get-ProbeOwner {
param ([string] $Path)
(Get-Acl -LiteralPath $Path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value
}
function New-ProbeFolder {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$path = Join-Path -Path $Root -ChildPath $Name
$null = New-Item -ItemType Directory -Path $path -Force
$path
}
function New-ProbePair {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$directory = New-ProbeFolder -Name $Name
foreach ($item in 'First', 'Second') {
Set-Content -LiteralPath (Join-Path -Path $directory -ChildPath "$item.txt") -Value $item -NoNewline
}
@{ Directory = $directory; First = (Join-Path -Path $directory -ChildPath 'First.txt'); Second = (Join-Path -Path $directory -ChildPath 'Second.txt') }
}
$cases = @{
'Remove-Item2' = @{
Prepare = { param ($Slug) New-ProbePair -Name "RemoveItem2-$Slug" }
Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Copy-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "CopyItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "CopyItem2-$Slug-To"; $c }
Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath 'Second.txt')) }
}
'Move-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "MoveItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "MoveItem2-$Slug-To"; $c }
Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Set-NTFSOwner' = @{
Prepare = { param ($Slug) New-ProbePair -Name "SetOwner-$Slug" }
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) (Get-ProbeOwner -Path $Context.Second) -eq $administrators }
}
'Set-NTFSSecurityDescriptor' = @{
Prepare = {
param ($Slug)
$c = New-ProbePair -Name "SetDescriptor-$Slug"
$c.Descriptors = @(Get-NTFSSecurityDescriptor -Path $c.First, $c.Second -ErrorAction Stop)
Add-NTFSAccess -SecurityDescriptor $c.Descriptors -Account $everyone -AccessRights ReadData -ErrorAction Stop
$c
}
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (@((Get-Acl -LiteralPath $Context.Second).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $everyone }).Count) }
}
}
$streamCases = @{
'Set-NTFSSecurityDescriptor/verbose' = @{
Prepare = $cases['Set-NTFSSecurityDescriptor'].Prepare
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -Verbose -ErrorAction SilentlyContinue 4>&1 }
Untouched = $cases['Set-NTFSSecurityDescriptor'].Untouched
RecordType = [System.Management.Automation.VerboseRecord]
}
'Set-NTFSOwner/debug' = @{
Prepare = $cases['Set-NTFSOwner'].Prepare
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -ErrorAction SilentlyContinue 5>&1 }
Untouched = $cases['Set-NTFSOwner'].Untouched
RecordType = [System.Management.Automation.DebugRecord]
}
}
function Assert-ProbePipelineStop {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$Error.Clear()
$result = @(& $Case.Run $context | Select-Object -First 1)
$result | Should -HaveCount 1
if ($Case.RecordType) {
$result[0] | Should -BeOfType $Case.RecordType
}
$Error.Count | Should -Be 0
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
function Assert-ProbeDownstreamFailure {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$emitted = 0
$caught = $null
$Error.Clear()
try {
& $Case.Run $context | ForEach-Object -Process {
$emitted++
throw 'Downstream failure'
}
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
$emitted | Should -Be 1
@($Error | Where-Object -FilterScript { $_.Exception.Message -notlike '*Downstream failure*' }) | Should -BeNullOrEmpty
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
}
Describe 'Mirror of the later-command tests' {
It '<Name> should stop after the first object for Select-Object -First 1' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbePipelineStop -Case $cases[$Name] -Slug 'Select'
}
It '<Name> should stop after the first object for throw' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbeDownstreamFailure -Case $cases[$Name] -Slug 'Throw'
}
It '<Key> should stop at the message for Select-Object -First 1' -ForEach @(
@{ Key = 'Set-NTFSSecurityDescriptor/verbose'; Stream = 'verbose' }, @{ Key = 'Set-NTFSOwner/debug'; Stream = 'debug' }
) {
Assert-ProbePipelineStop -Case $streamCases[$Key] -Slug ('{0}Select' -f $Stream) -Stream $Stream
}
}
} -Data @{ ModulePath = $ModulePath; Root = $root; Account = $account }
$configuration = New-PesterConfiguration
$configuration.Run.Container = $container
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Detailed'
$configuration.Output.RenderMode = 'Plaintext'
$lines.Add(('Edition {0} {1}; module {2}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion, $ModulePath))
$lines.Add('--- Pester output')
$output = & { Invoke-Pester -Configuration $configuration } *>&1
$result = @($output | Where-Object -FilterScript { $_ -is [Pester.Run] }) | Select-Object -First 1
foreach ($entry in @($output | Where-Object -FilterScript { $_ -isnot [Pester.Run] })) { $lines.Add('{0}' -f $entry) }
$lines.Add('--- Results')
foreach ($test in $result.Tests) {
$messages = @(@($test.ErrorRecord) | Where-Object -FilterScript { $_ } | ForEach-Object -Process { ($_.ToString() -split '\r?\n')[0] })
$lines.Add(('{0} | {1} | error records: {2} | {3}' -f $test.Result, $test.ExpandedName, @($test.ErrorRecord).Count, ($messages -join ' // ')))
}
$lines.Add(('Totals: passed {0}, failed {1}, not run {2}; result {3}' -f $result.PassedCount, $result.FailedCount, $result.NotRunCount, $result.Result))
$lines.Add('--- State of the items after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Sort-Object -Property Name) {
$files = @(Get-ChildItem -LiteralPath $folder.FullName -File | ForEach-Object -Process { $_.Name })
$lines.Add(('{0}: {1}' -f $folder.Name, ($files -join ', ')))
}
$lines.Add('--- Owner (SetOwner folders) and explicit entry for Everyone (SetDescriptor folders) after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Where-Object -FilterScript { $_.Name -like 'SetOwner-*' -or $_.Name -like 'SetDescriptor-*' } | Sort-Object -Property Name) {
foreach ($name in 'First.txt', 'Second.txt') {
$path = Join-Path -Path $folder.FullName -ChildPath $name
$acl = Get-Acl -LiteralPath $path
if ($folder.Name -like 'SetOwner-*') {
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
$lines.Add(('{0}\{1}: owner {2}' -f $folder.Name, $name, $(if ($owner -eq 'S-1-5-32-544') { 'Administrators (as created)' } elseif ($owner -eq $account) { 'the account of the run (changed)' } else { $owner })))
}
else {
$entries = @($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' })
$lines.Add(('{0}\{1}: explicit entry for Everyone: {2}' -f $folder.Name, $name, $(if ($entries.Count) { 'yes (changed)' } else { 'no (as created)' })))
}
}
}
}
finally {
$full = [System.IO.Path]::GetFullPath($root)
if ($full.StartsWith([System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()), [System.StringComparison]::OrdinalIgnoreCase) -and (Split-Path -Path $full -Leaf) -like 'mute-probe-*') {
Remove-Item -LiteralPath $full -Recurse -Force -ErrorAction SilentlyContinue
}
Set-Content -LiteralPath $OutFile -Value $lines -Encoding utf8
}

105
Tests/Lab/Acceptance/Repair-OsMatrixBoot.ps1

@ -0,0 +1,105 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $LogPath,
[Parameter(Mandatory)] [ValidatePattern('^[A-Za-z][A-Za-z0-9-]{0,14}$')] [string] $VmName,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[switch] $Start
)
# Repairs the boot files of one virtual machine of the matrix lab. AutomatedLab builds a base image with the bcdboot of the host and
# ignores its exit code; when the host's bcdboot can't process the boot files of an older image (it fails with exit code 193 on Windows
# Server 2019 and on Windows 11 22H2), the EFI system partition stays empty and the generation 2 virtual machine fails with Hyper-V event
# 18603. This script turns the machine off, mounts the machine's own differencing disk (never the shared base image), runs the bcdboot of
# the image itself, adds the removable-media path EFI\Boot\bootx64.efi that a new virtual machine boots from, checks the files, and
# dismounts the disk. It refuses a machine that isn't connected to the switch of the lab. Windows PowerShell 5.1 on the host, elevated.
$ErrorActionPreference = 'Stop'
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script must run in an elevated PowerShell session.' }
($stamp -f [DateTime]::UtcNow) + " START repair-os-matrix-boot vm=$VmName lab=$LabName" | Set-Content -LiteralPath $LogPath
$diskPath = $null
$letters = @()
try {
$vm = Get-VM -Name $VmName
if ($vm.Generation -ne 2) { throw "$VmName isn't a generation 2 machine." }
$switches = @(Get-VMNetworkAdapter -VMName $VmName | ForEach-Object -Process { $_.SwitchName })
# An array comparison with -ne returns the elements that differ, and an empty result is false: a machine without an adapter, or with an
# adapter that has no switch, would pass, so the guard counts.
if ($switches.Count -eq 0 -or @($switches | Where-Object -FilterScript { $_ -ne $LabName }).Count -gt 0) { throw "$VmName isn't connected only to the switch '$LabName' (switches: $($switches -join ', ')). Refusing." }
if ($vm.State -ne 'Off') {
Stop-VM -Name $VmName -TurnOff -Force
Write-Step "$VmName turned off"
}
$drive = Get-VMHardDiskDrive -VMName $VmName | Select-Object -First 1
$diskPath = $drive.Path
$vhd = Get-VHD -Path $diskPath
if ($vhd.VhdType -ne 'Differencing') { throw "$diskPath isn't a differencing disk; refusing to change a base image." }
Write-Step "disk $diskPath (parent $($vhd.ParentPath))"
$image = Mount-VHD -Path $diskPath -Passthru
$disk = $image | Get-Disk
$partitions = @(Get-Partition -DiskNumber $disk.Number)
$esp = $partitions | Where-Object -FilterScript { $_.GptType -eq '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' } | Select-Object -First 1
$system = $partitions | Where-Object -FilterScript { $_.Type -eq 'Basic' } | Sort-Object -Property Size -Descending | Select-Object -First 1
if (-not $esp -or -not $system) { throw 'The disk has no EFI system partition or no Windows partition.' }
foreach ($partition in $esp, $system) {
# The host may have assigned a letter to the Windows partition on mount already.
if (-not (Get-Partition -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber).DriveLetter) {
Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AssignDriveLetter
}
$letters += (Get-Partition -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber).DriveLetter
}
$espLetter = (Get-Partition -DiskNumber $disk.Number -PartitionNumber $esp.PartitionNumber).DriveLetter
$systemLetter = (Get-Partition -DiskNumber $disk.Number -PartitionNumber $system.PartitionNumber).DriveLetter
$windows = '{0}:\Windows' -f $systemLetter
$bcdboot = Join-Path -Path $windows -ChildPath 'System32\bcdboot.exe'
if (-not (Test-Path -LiteralPath $bcdboot)) { throw "$bcdboot is missing." }
$before = @(Get-ChildItem -LiteralPath ('{0}:\' -f $espLetter) -Recurse -Force -File -ErrorAction SilentlyContinue).Count
Write-Step ("system partition {0}: {1}; ESP {2}: holds {3} files; image build {4}" -f $systemLetter, $windows, $espLetter, $before, (Get-Item -LiteralPath $bcdboot).VersionInfo.ProductVersion)
$output = & $bcdboot $windows /s ('{0}:' -f $espLetter) /f UEFI 2>&1 | Out-String
Write-Step ("bcdboot of the image: exit code {0}: {1}" -f $LASTEXITCODE, ($output -replace '\s+', ' ').Trim())
if ($LASTEXITCODE -ne 0) { throw "bcdboot of the image failed with exit code $LASTEXITCODE." }
$bootManager = '{0}:\EFI\Microsoft\Boot\bootmgfw.efi' -f $espLetter
if (-not (Test-Path -LiteralPath $bootManager)) { throw "$bootManager is missing after bcdboot." }
$removable = '{0}:\EFI\Boot' -f $espLetter
$null = New-Item -ItemType Directory -Path $removable -Force
Copy-Item -LiteralPath $bootManager -Destination (Join-Path -Path $removable -ChildPath 'bootx64.efi') -Force
$after = @(Get-ChildItem -LiteralPath ('{0}:\' -f $espLetter) -Recurse -Force -File -ErrorAction SilentlyContinue).Count
$hasBcd = Test-Path -LiteralPath ('{0}:\EFI\Microsoft\Boot\BCD' -f $espLetter)
Write-Step ("ESP now holds {0} files; BCD present: {1}; bootx64.efi present: {2}" -f $after, $hasBcd, (Test-Path -LiteralPath (Join-Path -Path $removable -ChildPath 'bootx64.efi')))
if ($after -lt 20 -or -not $hasBcd) { throw "The EFI system partition still looks empty ($after files, BCD $hasBcd)." }
}
catch {
Write-Step ('repair-os-matrix-boot-FAILED: {0}' -f $_)
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
$failed = $true
}
finally {
if ($diskPath) {
try {
foreach ($partition in @(Get-Partition -DiskNumber (Get-VHD -Path $diskPath).DiskNumber -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.DriveLetter })) {
Remove-PartitionAccessPath -DiskNumber $partition.DiskNumber -PartitionNumber $partition.PartitionNumber -AccessPath ('{0}:\' -f $partition.DriveLetter) -ErrorAction SilentlyContinue
}
}
catch { Write-Step ('access path cleanup: {0}' -f $_) }
Dismount-VHD -Path $diskPath -ErrorAction SilentlyContinue
Write-Step 'disk dismounted'
}
}
if ($failed) { exit 1 }
if ($Start) {
Start-VM -Name $VmName
Write-Step "$VmName started"
}
Write-Step 'repair-os-matrix-boot-DONE'
exit 0

265
Tests/Lab/Acceptance/Run-MatrixLocalSuite.ps1

@ -0,0 +1,265 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The lab installation password comes from the AutomatedLab lab definition, which stores it as text; the credential is built in memory and never written.'
)]
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^[\w-]+$')] [string] $Label,
[Parameter(Mandatory)] [string] $Machine,
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutputRoot,
[string] $Edition = 'Desktop,Core',
[string] $Mode = 'Elevated',
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string] $LocalCredentialMachine = '',
[string] $PesterModulePath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1',
[string] $RepositoryRoot,
[ValidateRange(5, 480)] [int] $TimeoutMinutes = 90
)
# The module's own Pester suite on the machines of the operating-system matrix (Decision 24), in Windows PowerShell 5.1 on the Hyper-V
# host. The live controller proves the behavior against a domain and remote servers; this proves the module and its tests run on each
# operating system and edition. It stages the behavior test files of the repository and the module under test (the same bits for every
# machine), copies them to the machine, runs Invoke-LocalSuite.ps1 in a new Windows PowerShell and a new PowerShell 7 process one after
# the other, and copies the log, the NUnit result, and the JSON summary back. -Mode Basic runs each edition with the token of a basic
# user, the way .github\scripts\Invoke-TestsAsBasicUser.ps1 does (the class of that script is extracted, not copied): the tests that
# need a missing privilege skip in the elevated mode and run in this one. The machine name LOCAL runs the same stage on this host, as
# the reference. A machine that can't use the domain account (a Windows 11 build whose secure channel to the domain controller fails) is
# listed in -LocalCredentialMachine and reached with the local installation account. Nothing secret is written.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 leaves $PSScriptRoot empty in a parameter default when the script runs with -File.
if (-not $RepositoryRoot) { $RepositoryRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent }
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
$targets = @($Machine -split ',' | Where-Object -FilterScript { $_ })
$editions = @($Edition -split ',' | Where-Object -FilterScript { $_ })
$modes = @($Mode -split ',' | Where-Object -FilterScript { $_ })
if ($modes | Where-Object -FilterScript { $_ -notin 'Elevated', 'Basic' }) { throw "-Mode takes Elevated, Basic, or both, separated by a comma." }
$localCredential = @($LocalCredentialMachine -split ',' | Where-Object -FilterScript { $_ })
$behaviorFiles = 'Access', 'Audit', 'DriveRoot', 'FileHash', 'Inheritance', 'ItemCmdlets', 'Links', 'ObjectApis', 'OutputTypes', 'Owner', 'PathErrors',
'PermissionScopes', 'PipelineControl', 'Privileges', 'Remove-Item2', 'SecurityDescriptor', 'SecurityDescriptorSets', 'TestHelpers'
$null = New-Item -ItemType Directory -Path $OutputRoot -Force
$sequenceLog = Join-Path -Path $OutputRoot -ChildPath "$Label-localsuite.log"
function Write-Sequence { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $sequenceLog }
$stage = Join-Path -Path $env:TEMP -ChildPath "ntfs-localsuite-$Label"
if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
$null = New-Item -ItemType Directory -Path (Join-Path -Path $stage -ChildPath 'Tests') -Force
$null = New-Item -ItemType Directory -Path (Join-Path -Path $stage -ChildPath 'NTFSSecurity\bin\Release') -Force
foreach ($name in $behaviorFiles) {
$file = if ($name -eq 'TestHelpers') { 'TestHelpers.Tests.ps1' } else { "$name.Tests.ps1" }
Copy-Item -LiteralPath (Join-Path -Path $RepositoryRoot -ChildPath "Tests\$file") -Destination (Join-Path -Path $stage -ChildPath 'Tests')
}
Copy-Item -LiteralPath (Join-Path -Path $RepositoryRoot -ChildPath 'Tests\TestHelpers.psm1') -Destination (Join-Path -Path $stage -ChildPath 'Tests')
Copy-Item -Path (Join-Path -Path $ModulePath -ChildPath '*') -Destination (Join-Path -Path $stage -ChildPath 'NTFSSecurity\bin\Release') -Recurse
Copy-Item -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath 'Invoke-LocalSuite.ps1') -Destination $stage
if ('Basic' -in $modes) {
$wrapper = Get-Content -LiteralPath (Join-Path -Path $RepositoryRoot -ChildPath '.github\scripts\Invoke-TestsAsBasicUser.ps1') -Raw
$class = [regex]::Match($wrapper, "(?s)Add-Type -TypeDefinition @'\r?\n(.*?)\r?\n'@").Groups[1].Value
if (-not $class) { throw 'The class of the basic-user wrapper was not found in .github\scripts\Invoke-TestsAsBasicUser.ps1.' }
$helperHead = @'
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $Executable,
[Parameter(Mandatory)] [string] $Root,
[Parameter(Mandatory)] [string] $Label,
[Parameter(Mandatory)] [string] $OutDir,
[string] $PesterModulePath
)
# Generated by Run-MatrixLocalSuite.ps1: starts Invoke-LocalSuite.ps1 with the token of a basic user (SAFER level Normal User) through the
# class of .github\scripts\Invoke-TestsAsBasicUser.ps1, waits for it, and writes its exit code.
$ErrorActionPreference = 'Stop'
'@
$helperTail = @'
$runnerArguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" -Label {1} -Root "{2}" -OutDir "{3}"' -f (Join-Path -Path $Root -ChildPath 'Invoke-LocalSuite.ps1'), $Label, $Root, $OutDir
if ($PesterModulePath) { $runnerArguments += ' -PesterModulePath "{0}"' -f $PesterModulePath }
$console = Join-Path -Path $OutDir -ChildPath ('{0}.console.txt' -f $Label)
$commandLine = 'cmd.exe /d /s /c ""{0}" {1} > "{2}" 2>&1"' -f $Executable, $runnerArguments, $console
$exitCode = [NTFSSecurityBasicUserProcess]::Run((Join-Path -Path $env:SystemRoot -ChildPath 'System32\cmd.exe'), $commandLine, $Root)
Set-Content -LiteralPath (Join-Path -Path $OutDir -ChildPath ('{0}.basic.exit' -f $Label)) -Value $exitCode
exit $exitCode
'@
$helperText = $helperHead + "`r`nAdd-Type -TypeDefinition @'`r`n" + $class + "`r`n'@`r`n" + $helperTail
Set-Content -LiteralPath (Join-Path -Path $stage -ChildPath 'Start-BasicUserProcess.ps1') -Value $helperText -Encoding UTF8
}
$dllHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.dll')).Hash
$testHashes = Get-ChildItem -LiteralPath (Join-Path -Path $stage -ChildPath 'Tests') -File | Sort-Object -Property Name | ForEach-Object -Process { '{0}={1}' -f $_.Name, (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash.Substring(0, 12) }
($stamp -f [DateTime]::UtcNow) + " START localsuite-$Label machines=$($targets -join ',') editions=$($editions -join ',') dll=$dllHash" | Set-Content -LiteralPath $sequenceLog
Write-Sequence ('staged test files: {0}' -f ($testHashes -join ' '))
$summaryRows = New-Object -TypeName 'System.Collections.Generic.List[object]'
if ($targets | Where-Object -FilterScript { $_ -ne 'LOCAL' }) {
Import-Module -Name AutomatedLab -ErrorAction Stop
Import-Lab -Name $LabName -NoValidation -NoDisplay
}
foreach ($name in $targets) {
$cellFolder = Join-Path -Path $OutputRoot -ChildPath "$Label-$name"
$null = New-Item -ItemType Directory -Path $cellFolder -Force
Write-Sequence "machine $name START"
$session = $null
$runCredential = $null
$resultsCopied = $false
try {
if ($name -eq 'LOCAL') {
$root = Join-Path -Path $env:TEMP -ChildPath "ntfs-localsuite-run-$Label"
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
Copy-Item -LiteralPath $stage -Destination $root -Recurse
}
else {
$sessionParameters = @{ ComputerName = $name }
if ($name -in $localCredential) { $sessionParameters.UseLocalCredential = $true }
$session = New-LabPSSession @sessionParameters
# The account for the scheduled tasks: the lab account of the machine, or its local installation account. AutomatedLab keeps the
# installation password in clear text in the lab file; here it stays in memory.
$machineDefinition = Get-LabVM -ComputerName $name
$runCredential = if ($name -in $localCredential) {
New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList ('{0}\{1}' -f $name, $machineDefinition.InstallationUser.UserName), (ConvertTo-SecureString -String $machineDefinition.InstallationUser.Password -AsPlainText -Force)
}
else {
$machineDefinition.GetCredential((Get-Lab))
}
$root = 'C:\NtfsMatrixLocal\' + $Label
Invoke-Command -Session $session -ArgumentList $root -ScriptBlock {
param ($Path)
if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Recurse -Force }
$null = New-Item -ItemType Directory -Path $Path -Force
}
Copy-Item -Path (Join-Path -Path $stage -ChildPath '*') -Destination $root -ToSession $session -Recurse -Force
Write-Sequence "machine $name stage copied to $root"
}
foreach ($modeName in $modes) {
foreach ($editionName in $editions) {
$runLabel = ('{0}-{1}-{2}{3}' -f $Label, $name, $editionName, $(if ($modeName -eq 'Basic') { '-basic' } else { '' })).ToLowerInvariant()
$arguments = @{ Root = $root; Edition = $editionName; RunLabel = $runLabel; Pester = $(if ($name -eq 'LOCAL') { $PesterModulePath } else { '' }); Mode = $modeName }
$start = {
param ($Root, $Edition, $RunLabel, $Pester, $ModeName, $Credential)
$exe = if ($Edition -eq 'Desktop') { Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe' } else { Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe' }
$out = Join-Path -Path $Root -ChildPath 'Results'
$null = New-Item -ItemType Directory -Path $out -Force
if ($ModeName -eq 'Basic') {
# The basic-user token writes the results, so the account of the run needs Modify on the folder.
$null = & icacls.exe $out /grant ('{0}:(OI)(CI)M' -f [Security.Principal.WindowsIdentity]::GetCurrent().Name)
$list = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f (Join-Path -Path $Root -ChildPath 'Start-BasicUserProcess.ps1')),
'-Executable', ('"{0}"' -f $exe), '-Root', ('"{0}"' -f $Root), '-Label', $RunLabel, '-OutDir', ('"{0}"' -f $out))
if ($Pester) { $list += '-PesterModulePath', ('"{0}"' -f $Pester) }
$exe = Join-Path -Path $env:SystemRoot -ChildPath 'System32\WindowsPowerShell\v1.0\powershell.exe'
}
else {
$list = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f (Join-Path -Path $Root -ChildPath 'Invoke-LocalSuite.ps1')),
'-Label', $RunLabel, '-Root', ('"{0}"' -f $Root), '-OutDir', ('"{0}"' -f $out))
if ($Pester) { $list += '-PesterModulePath', ('"{0}"' -f $Pester) }
}
if ($Credential) {
# A process started from a remoting session inherits a token with every privilege enabled and no credentials of its own,
# which the tests don't expect (eight of them fail). A scheduled task with a batch logon at the highest run level gets
# the token of an elevated interactive session: privileges present but disabled, and the credentials of the account.
$taskName = 'NtfsMatrixLocal-' + $RunLabel
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute $exe -Argument ($list -join ' ')
$null = Register-ScheduledTask -TaskName $taskName -Action $action -RunLevel Highest -User $Credential.UserName -Password $Credential.GetNetworkCredential().Password
Start-ScheduledTask -TaskName $taskName
$taskName
}
else {
(Start-Process -FilePath $exe -ArgumentList $list -PassThru -WindowStyle Hidden).Id
}
}
$isRunning = {
param ($Handle)
if ($Handle -is [string]) { $task = Get-ScheduledTask -TaskName $Handle -ErrorAction SilentlyContinue; [bool] ($task -and $task.State -eq 'Running') }
else { [bool] (Get-Process -Id $Handle -ErrorAction SilentlyContinue) }
}
$stop = {
param ($Handle)
if ($Handle -is [string]) { Stop-ScheduledTask -TaskName $Handle -ErrorAction SilentlyContinue } else { Stop-Process -Id $Handle -Force -ErrorAction SilentlyContinue }
}
$finish = {
param ($Handle)
if ($Handle -is [string]) {
$result = (Get-ScheduledTaskInfo -TaskName $Handle -ErrorAction SilentlyContinue).LastTaskResult
Unregister-ScheduledTask -TaskName $Handle -Confirm:$false -ErrorAction SilentlyContinue
"task result $result"
}
}
$startArguments = $arguments.Root, $arguments.Edition, $arguments.RunLabel, $arguments.Pester, $arguments.Mode, $runCredential
$handle = if ($session) { Invoke-Command -Session $session -ScriptBlock $start -ArgumentList $startArguments } else { & $start @startArguments }
Write-Sequence "machine $name $modeName $editionName started ($handle)"
$deadline = [DateTime]::UtcNow.AddMinutes($TimeoutMinutes)
do {
Start-Sleep -Seconds 20
$alive = if ($session) { Invoke-Command -Session $session -ScriptBlock $isRunning -ArgumentList $handle } else { & $isRunning $handle }
} while ($alive -and [DateTime]::UtcNow -lt $deadline)
if ($alive) {
if ($session) { Invoke-Command -Session $session -ScriptBlock $stop -ArgumentList $handle } else { & $stop $handle }
Write-Sequence "machine $name $modeName $editionName TIMED OUT after $TimeoutMinutes minutes; stopped"
}
$outcome = if ($session) { Invoke-Command -Session $session -ScriptBlock $finish -ArgumentList $handle } else { & $finish $handle }
Write-Sequence "machine $name $modeName $editionName finished $outcome"
}
}
if ($session) { Copy-Item -FromSession $session -Path (Join-Path -Path $root -ChildPath 'Results\*') -Destination $cellFolder -Recurse -Force }
else { Copy-Item -Path (Join-Path -Path $root -ChildPath 'Results\*') -Destination $cellFolder -Recurse -Force }
$resultsCopied = $true
foreach ($modeName in $modes) {
foreach ($editionName in $editions) {
$expected = ('{0}-{1}-{2}{3}' -f $Label, $name, $editionName, $(if ($modeName -eq 'Basic') { '-basic' } else { '' })).ToLowerInvariant()
if (-not (Test-Path -LiteralPath (Join-Path -Path $cellFolder -ChildPath "$expected.json"))) {
Write-Sequence "machine ${name}: NO RESULT FILE for $expected"
$summaryRows.Add([pscustomobject]@{ Machine = $name; Edition = $editionName; Os = ''; PowerShell = ''; Elevated = ''; Result = 'NoResult'; Passed = ''; Failed = ''; Skipped = ''; Total = ''; Seconds = '' })
}
}
}
foreach ($json in Get-ChildItem -LiteralPath $cellFolder -Filter '*.json') {
$summary = Get-Content -LiteralPath $json.FullName -Raw | ConvertFrom-Json
$summaryRows.Add([pscustomobject]@{
Machine = $name; Edition = $summary.Edition; Os = $summary.Os; PowerShell = $summary.PowerShell; Elevated = $summary.Elevated; Result = $summary.Result
Passed = $summary.Passed; Failed = $summary.Failed; Skipped = $summary.Skipped; Total = $summary.Total; Seconds = $summary.Seconds
})
Write-Sequence ('machine {0} {1}: {2} passed={3} failed={4} skipped={5} total={6} elevated={7} os={8}' -f $name, $summary.Edition, $summary.Result, $summary.Passed, $summary.Failed, $summary.Skipped, $summary.Total, $summary.Elevated, $summary.Os)
}
}
catch {
Write-Sequence "machine $name FAILED: $_"
}
finally {
if ($session) {
# The tasks of this run store the password of the account that runs them. A run that stops early must not leave them on the machine.
try {
Invoke-Command -Session $session -ArgumentList ('NtfsMatrixLocal-{0}-*' -f $Label.ToLowerInvariant()) -ScriptBlock {
param ($Pattern)
Get-ScheduledTask -TaskName $Pattern -ErrorAction SilentlyContinue | ForEach-Object -Process { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -ErrorAction SilentlyContinue }
}
}
catch {
Write-Sequence "machine ${name}: the scheduled tasks of this run could not be removed: $($_.Exception.Message)"
}
# The results are on the host, so the stage on the machine (the module, the tests, and the logs) is not needed any more. After an
# early stop it stays for the diagnosis.
if ($resultsCopied) {
try {
Invoke-Command -Session $session -ArgumentList $root -ScriptBlock { param ($Path) Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue }
}
catch {
Write-Sequence "machine ${name}: the stage $root could not be removed: $($_.Exception.Message)"
}
}
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
}
}
Write-Sequence "machine $name END"
}
$summaryRows | Export-Csv -LiteralPath (Join-Path -Path $OutputRoot -ChildPath "$Label-localsuite-summary.csv") -NoTypeInformation
Write-Sequence "localsuite-$Label-DONE"
exit 0

105
Tests/Lab/Acceptance/Run-MatrixSequence.ps1

@ -0,0 +1,105 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^[\w-]+$')] [string] $Label,
[Parameter(Mandatory)] [string] $FileServer,
[string] $Client = 'OSWin11E',
[string] $ModulePath,
[string] $Version,
[string] $Edition = 'Desktop,Core',
[Parameter(Mandatory)] [string] $OutputRoot,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string] $DomainController = 'OSDC1',
[string] $LabFolder,
[string] $Machines = 'OSFile19,OSFile22,OSFile25,OSWin11E'
)
# One detached sequence of the operating-system matrix (Decision 24) in Windows PowerShell 5.1 on the Hyper-V host. For each cell, a file
# server with the client, it runs: readiness of every machine, the unmodified controller of the repository for the source (a build
# folder or an exact Gallery version) in both editions, the validation of every role from the result files, a snapshot of the fixture
# SIDs, the removal of the fixture, and an independent check of the end state. An infrastructure failure (no summary of the controller)
# stops the later cells; failing tests don't. Case 9 (accounts of other forests) needs trusts that this lab doesn't have, so the cells
# run with -ForeignDomainController @(). Nothing secret is written: the controller keeps the passwords in memory.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 leaves $PSScriptRoot empty in a parameter default when the script runs with -File.
if (-not $LabFolder) { $LabFolder = Split-Path -Path $PSScriptRoot -Parent }
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
$kit = $PSScriptRoot
$cells = @($FileServer -split ',' | Where-Object -FilterScript { $_ })
$editions = @($Edition -split ',' | Where-Object -FilterScript { $_ })
$allMachines = @($Machines -split ',' | Where-Object -FilterScript { $_ })
if ([bool] $ModulePath -eq [bool] $Version) { throw 'Pass exactly one of -ModulePath and -Version.' }
New-Item -ItemType Directory -Path $OutputRoot -Force | Out-Null
$sequenceLog = Join-Path -Path $OutputRoot -ChildPath "$Label-sequence.log"
function Write-Sequence { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $sequenceLog }
$source = if ($ModulePath) { "module=$ModulePath dll=$((Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.dll')).Hash)" } else { "version=$Version" }
($stamp -f [DateTime]::UtcNow) + " START matrix-sequence-$Label client=$Client cells=$($cells -join ',') editions=$($editions -join ',') $source" | Set-Content -LiteralPath $sequenceLog
Write-Sequence ('controller blob {0}' -f ((& git -C (Split-Path -Path (Split-Path -Path $LabFolder -Parent) -Parent) hash-object (Join-Path -Path $LabFolder -ChildPath 'Invoke-NTFSSecurityLabTest.ps1')) -join ''))
Write-Sequence ('live tests blob {0}' -f ((& git -C (Split-Path -Path (Split-Path -Path $LabFolder -Parent) -Parent) hash-object (Join-Path -Path $LabFolder -ChildPath 'NTFSSecurity.Live.Tests.ps1')) -join ''))
$controller = Join-Path -Path $LabFolder -ChildPath 'Invoke-NTFSSecurityLabTest.ps1'
$infrastructureFailed = $false
foreach ($fileServerName in $cells) {
if ($infrastructureFailed) { Write-Sequence "cell $fileServerName SKIPPED after an infrastructure failure"; continue }
$cell = Join-Path -Path $OutputRoot -ChildPath "$Label-$fileServerName"
New-Item -ItemType Directory -Path $cell -Force | Out-Null
$runLog = Join-Path -Path $cell -ChildPath 'run.log'
$ran = $false
Write-Sequence "cell $fileServerName START (client $Client)"
try {
$readinessLog = Join-Path -Path $cell -ChildPath 'readiness.log'
& (Join-Path -Path $kit -ChildPath 'Test-MatrixReadiness.ps1') -LabName $LabName -DomainController @($DomainController) -Member @($allMachines) -OutFile $readinessLog
$readiness = Get-Content -LiteralPath $readinessLog -Raw
$notReady = 'wsman=failed|secure channel False|PowerShell 7 missing|Core missing|Pester Desktop (?!5\.7\.1)|=False|kerberos: .*Error'
$problem = [regex]::Match($readiness, $notReady).Value
if ($readiness -notmatch 'matrix-readiness-DONE' -or $problem) { throw "Readiness of cell $fileServerName failed ('$problem'); see $readinessLog" }
Write-Sequence "cell $fileServerName readiness ok"
$arguments = @{
LabName = $LabName; DomainController = $DomainController; FileServer = $fileServerName; Client = $Client
ForeignDomainController = @(); Edition = $editions; OutputPath = $cell; Confirm = $false
}
if ($ModulePath) { $arguments.Version = @(); $arguments.ModulePath = $ModulePath } else { $arguments.Version = @($Version) }
($stamp -f [DateTime]::UtcNow) + " START controller cell=$fileServerName" | Set-Content -LiteralPath $runLog
$ran = $true
& { & $controller @arguments } *>&1 | ForEach-Object -Process { '{0}' -f $_ } | Out-File -LiteralPath $runLog -Append -Encoding utf8 -Width 500
$summaryPath = Get-ChildItem -LiteralPath (Join-Path -Path $cell -ChildPath 'Results') -Filter 'Summary.json' -Recurse -ErrorAction SilentlyContinue |
Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1 -ExpandProperty FullName
if (-not $summaryPath) { throw "The controller wrote no Summary.json for cell $fileServerName; see $runLog" }
Write-Sequence "cell $fileServerName controller done: $summaryPath"
$validationLog = Join-Path -Path $cell -ChildPath 'validation.log'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path -Path $kit -ChildPath 'Validate-LabResults.ps1') -ResultsFolder (Split-Path -Path $summaryPath -Parent) -OutputPrefix (Join-Path -Path $cell -ChildPath "$Label-$fileServerName") -Edition ($editions -join ',') -Expect Candidate *>&1 |
Out-File -LiteralPath $validationLog -Encoding utf8 -Width 400
Write-Sequence "cell $fileServerName validation exit code $LASTEXITCODE (see validation.log)"
}
catch {
Write-Sequence "cell $fileServerName FAILED before the cleanup: $_"
if (-not $ran) { Write-Sequence "cell ${fileServerName}: the controller did not start" }
$infrastructureFailed = $true
}
try {
$sidFile = Join-Path -Path $cell -ChildPath 'fixture-sids.json'
$common = @{ LabName = $LabName; DomainController = @($DomainController); Machine = @($allMachines) }
if ($ran) {
& (Join-Path -Path $kit -ChildPath 'Test-MatrixCleanup.ps1') -Mode Snapshot -SidFile $sidFile -OutFile (Join-Path -Path $cell -ChildPath 'cleanup-1-snapshot.log') @common
& { & $controller -RemoveFixture -LabName $LabName -DomainController $DomainController -FileServer $fileServerName -Client $Client -ForeignDomainController @() -Confirm:$false } *>&1 |
ForEach-Object -Process { '{0}' -f $_ } | Out-File -LiteralPath (Join-Path -Path $cell -ChildPath 'cleanup-2-remove.log') -Encoding utf8 -Width 500
& (Join-Path -Path $kit -ChildPath 'Test-MatrixCleanup.ps1') -Mode Verify -SidFile $sidFile -OutFile (Join-Path -Path $cell -ChildPath 'cleanup-3-verify.log') @common
$verify = Get-Content -LiteralPath (Join-Path -Path $cell -ChildPath 'cleanup-3-verify.log') -Raw
$clean = ($verify -match 'matrix-cleanup-Verify-DONE') -and ($verify -notmatch 'OU NTFSSecurityLive: True') -and ($verify -notmatch 'accounts: NtfsLive') -and
($verify -notmatch 'share=True|C:\\NTFSSecurityLive=True|C:\\NTFSSecurityLab=True|NtfsLiveLocal=True') -and ($verify -notmatch 'profiles=[1-9]') -and ($verify -notmatch ': [1-9]\d* fixture member') -and
($verify -notmatch 'probe accounts: [1-9]') -and ($verify -notmatch 'residue: [^\r\n]*=[1-9]')
Write-Sequence ("cell $fileServerName cleanup verdict from the verify log: {0}" -f $(if ($clean) { 'CLEAN' } else { 'DIRTY (read cleanup-3-verify.log)' }))
}
}
catch {
Write-Sequence "cell $fileServerName cleanup FAILED: $_"
}
Write-Sequence "cell $fileServerName END"
}
Write-Sequence "matrix-sequence-$Label-DONE"
exit 0

190
Tests/Lab/Acceptance/Test-MatrixCleanup.ps1

@ -0,0 +1,190 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidateSet('Snapshot', 'Verify', 'Repair')] [string] $Mode,
[Parameter(Mandatory)] [string] $SidFile,
[Parameter(Mandatory)] [string] $OutFile,
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string[]] $DomainController = @('OSDC1'),
[string[]] $Machine = @('OSFile19', 'OSFile22', 'OSFile25', 'OSWin11E')
)
# Independent end-state check of the fixture of Invoke-NTFSSecurityLabTest.ps1 in a lab (Windows PowerShell 5.1, on the host). Snapshot
# records the SIDs of the NtfsLive* accounts while the fixture exists. Verify reads the domains and every machine again, and reports
# the organizational unit, the accounts, the share, the folders, the local group, the memberships of Administrators, Access Control
# Assistance Operators, and Remote Management Users, and the profiles of those SIDs, and what the suite runs and the probes of the kit leave
# behind (scheduled tasks, items in the stage folders, the folders of the account probe, standard users NtfsProbe* with their profiles and their
# entries in Performance Log Users, probe accounts of the domain). The result is judged from this log, never from the wrapper of the controller
# or a global error count. Repair is for a run whose removal failed: with the SIDs of the snapshot, it removes what that run left on the machines
# (the memberships, also of orphaned SIDs, which net localgroup deletes by SID; the share; the local group; the folders) and what the kit leaves
# (the items in the stage folders, the folders of the account probe, the scheduled tasks NtfsMatrix*, the standard users NtfsProbe* with their
# profiles and their entries in Performance Log Users, and the domain accounts NtfsProbe*), and then reports like Verify. The patterns are the
# prefixes of the kit, matched in the whole domain and on the whole machine, not only in the organizational unit and the folders of the kit:
# every AD object whose sAMAccountName starts with NtfsProbe (a computer account too), the NtfsLive* objects of the domain (their SIDs go to the
# snapshot), every local-group member whose name contains NtfsLive, every scheduled task NtfsMatrix*, every local user NtfsProbe* and its profile
# folder, and every unresolved S-1-5-21-* member of Performance Log Users (a real principal of a trust that is down shows as one). The probe is the
# only writer of that group in these labs and uses the same pattern in its own cleanup. Run Repair only in a lab where nothing else has these
# names or leaves such members: Verify reports them, and Repair removes them.
& {
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$DomainController = @($DomainController | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$Machine = @($Machine | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
'[{0:yyyy-MM-dd HH:mm:ss}Z] START matrix-cleanup-{1} lab={2}' -f [DateTime]::UtcNow, $Mode, $LabName
Import-Lab -Name $LabName -NoValidation -NoDisplay
$labCommand = @{ NoDisplay = $true; PassThru = $true; ErrorAction = 'Stop' }
if ($Mode -eq 'Repair') {
# The accounts that the probes of the kit create in the domain, by their prefix; this runs before the directory is read, so that the report shows the result.
$repairDirectoryScript = {
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$objects = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsProbe*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator)
foreach ($object in $objects) { Remove-ADObject -Identity $object -Recursive -Confirm:$false -Server $domain.PDCEmulator }
'{0}: removed {1} account(s) named NtfsProbe*' -f $domain.DNSRoot, $objects.Count
}
foreach ($name in $DomainController) {
foreach ($message in @(Invoke-LabCommand -ComputerName $name -ActivityName "Repair the directory of $name" -ScriptBlock $repairDirectoryScript @labCommand)) {
'{0,-9} repair: {1}' -f $name, $message
}
}
}
$directoryScript = {
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$unit = Get-ADOrganizationalUnit -LDAPFilter '(ou=NTFSSecurityLive)' -SearchBase $domain.DistinguishedName -SearchScope OneLevel -Server $domain.PDCEmulator
[pscustomobject]@{
Domain = $domain.DNSRoot
Unit = [bool] $unit
Sids = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsLive*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator -Properties objectSid, sAMAccountName |
ForEach-Object -Process { '{0}={1}' -f $_.sAMAccountName, $_.objectSid.Value })
ProbeAccounts = @(Get-ADObject -LDAPFilter '(sAMAccountName=NtfsProbe*)' -SearchBase $domain.DistinguishedName -Server $domain.PDCEmulator).Count
}
}
$directory = @(foreach ($name in $DomainController) { Invoke-LabCommand -ComputerName $name -ActivityName "Read the fixture of $name" -ScriptBlock $directoryScript @labCommand })
foreach ($state in $directory) {
'{0,-14} OU NTFSSecurityLive: {1,-5} NtfsLive* accounts: {2}; probe accounts: {3}' -f $state.Domain, $state.Unit, ($(if ($state.Sids) { $state.Sids -join ', ' } else { 'none' })), $state.ProbeAccounts
}
if ($Mode -eq 'Snapshot') {
$sids = @($directory | ForEach-Object -Process { $_.Sids } | ForEach-Object -Process { ($_ -split '=', 2)[1] })
ConvertTo-Json -InputObject $sids | Set-Content -LiteralPath $SidFile -Encoding utf8
"saved $($sids.Count) SIDs to $SidFile"
}
else {
$sids = [string[]] (Get-Content -LiteralPath $SidFile -Raw | ConvertFrom-Json)
"checking $($sids.Count) SIDs of the snapshot"
$machineScript = {
param ($Sid)
# net localgroup lists an orphaned SID, which Get-LocalGroupMember in Windows PowerShell 5.1 fails on and skips.
$groups = foreach ($groupSid in 'S-1-5-32-544', 'S-1-5-32-579', 'S-1-5-32-580') {
$groupName = ([System.Security.Principal.SecurityIdentifier] $groupSid).Translate([System.Security.Principal.NTAccount]).Value -replace '^.*\\', ''
$members = @(& net.exe localgroup $groupName 2>&1 | ForEach-Object -Process { "$_".Trim() })
$hits = @($members | Where-Object -FilterScript { $_ -match 'NtfsLive' -or $_ -in $Sid })
'{0}: {1} fixture member(s)' -f $groupSid, $hits.Count
}
# What the account probe leaves: the profiles and the profile folders of its users, and its entries in Performance Log Users. net.exe lists a
# local user by its bare name, and an entry of a deleted domain account as its SID, or as its name for a while (the cache of names).
$usersFolder = Join-Path -Path $env:SystemDrive -ChildPath 'Users'
$probePaths = @(@(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.LocalPath -like (Join-Path -Path $usersFolder -ChildPath 'NtfsProbe*') } | ForEach-Object -Process { $_.LocalPath }) +
@(Get-ChildItem -LiteralPath $usersFolder -Filter 'NtfsProbe*' -Force -ErrorAction SilentlyContinue | ForEach-Object -Process { $_.FullName }) | Sort-Object -Unique)
$logGroup = ([System.Security.Principal.SecurityIdentifier] 'S-1-5-32-559').Translate([System.Security.Principal.NTAccount]).Value -replace '^.*\\', ''
$probeMembers = @(& net.exe localgroup $logGroup 2>&1 | ForEach-Object -Process { "$_".Trim() } | Where-Object -FilterScript { $_ -match '^S-1-5-21-[\d-]+$' -or $_ -match 'NtfsProbe' })
[pscustomobject]@{
Share = [bool] (Get-SmbShare -Name 'NTFSSecurityLive' -ErrorAction SilentlyContinue)
ShareRoot = Test-Path -LiteralPath 'C:\NTFSSecurityLive'
Payload = Test-Path -LiteralPath 'C:\NTFSSecurityLab'
LocalGroup = [bool] (Get-LocalGroup -Name 'NtfsLiveLocal' -ErrorAction SilentlyContinue)
Groups = $groups -join '; '
Profiles = @(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.SID -in $Sid }).Count
# What the suite runs and the probes of the kit leave behind: scheduled tasks, items in the stage folders, the folders of the
# account probe, and standard users
Tasks = @(Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.TaskName -like 'NtfsMatrix*' }).Count
Stages = @('C:\NtfsMatrixLocal', 'C:\NtfsMatrixProbe' | Where-Object -FilterScript { Test-Path -LiteralPath $_ } | ForEach-Object -Process { Get-ChildItem -LiteralPath $_ -Force -ErrorAction SilentlyContinue }).Count +
@('C:\NtfsProbeRecreation', 'C:\NtfsProbeModules' | Where-Object -FilterScript { Test-Path -LiteralPath $_ }).Count
Users = @(Get-LocalUser -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Name -like 'NtfsProbe*' }).Count
ProbeProfiles = $probePaths.Count
ProbeMembers = $probeMembers.Count
}
}
foreach ($name in $Machine) {
if ($Mode -eq 'Repair') {
$repairScript = {
param ($Sid)
$messages = New-Object -TypeName 'System.Collections.Generic.List[string]'
foreach ($groupSid in 'S-1-5-32-544', 'S-1-5-32-579', 'S-1-5-32-580') {
$groupName = ([System.Security.Principal.SecurityIdentifier] $groupSid).Translate([System.Security.Principal.NTAccount]).Value -replace '^.*\\', ''
$named = @(& net.exe localgroup $groupName 2>&1 | ForEach-Object -Process { "$_".Trim() } | Where-Object -FilterScript { $_ -match 'NtfsLive' })
foreach ($member in @($Sid) + $named) { $null = & net.exe localgroup $groupName $member /delete 2>&1 }
}
if (Get-SmbShare -Name 'NTFSSecurityLive' -ErrorAction SilentlyContinue) { Remove-SmbShare -Name 'NTFSSecurityLive' -Force }
$null = & net.exe localgroup 'NtfsLiveLocal' /delete 2>&1
foreach ($path in 'C:\NTFSSecurityLive', 'C:\NTFSSecurityLab') {
$command = '$errors = @(); Remove-Item -LiteralPath ''__PATH__'' -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable errors; $errors | ForEach-Object -Process { "$_" }'.Replace('__PATH__', $path)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
$attempt = 0
while ((Test-Path -LiteralPath $path) -and $attempt -lt 6) {
$attempt++
if ($attempt -gt 1) { Start-Sleep -Seconds 5 }
$null = & (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe') -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1
}
$messages.Add(('{0}: present after {1} attempt(s): {2}' -f $path, $attempt, (Test-Path -LiteralPath $path)))
}
# What the suite runner and the probes of the kit left: the items in their stage folders, the folders of the account probe,
# their scheduled tasks, and the standard users that the probe of the authorization managers creates (with their profiles)
foreach ($stage in 'C:\NtfsMatrixLocal', 'C:\NtfsMatrixProbe') {
if (Test-Path -LiteralPath $stage) { Get-ChildItem -LiteralPath $stage -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }
}
foreach ($folder in 'C:\NtfsProbeRecreation', 'C:\NtfsProbeModules') {
if (Test-Path -LiteralPath $folder) { Remove-Item -LiteralPath $folder -Recurse -Force -ErrorAction SilentlyContinue }
}
Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.TaskName -like 'NtfsMatrix*' } | ForEach-Object -Process { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -ErrorAction SilentlyContinue }
foreach ($user in @(Get-LocalUser -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.Name -like 'NtfsProbe*' })) { Remove-LocalUser -SID $user.SID -ErrorAction SilentlyContinue }
# The entries of the probe in Performance Log Users go by SID or name through the cmdlet: net.exe doesn't take the SID of an account that its name cache still resolves.
$logGroup = ([System.Security.Principal.SecurityIdentifier] 'S-1-5-32-559').Translate([System.Security.Principal.NTAccount]).Value -replace '^.*\\', ''
foreach ($member in @(& net.exe localgroup $logGroup 2>&1 | ForEach-Object -Process { "$_".Trim() } | Where-Object -FilterScript { $_ -match '^S-1-5-21-[\d-]+$' -or $_ -match 'NtfsProbe' })) {
Remove-LocalGroupMember -SID 'S-1-5-32-559' -Member $member -ErrorAction SilentlyContinue
}
# A profile that the last task of a probe user used stays loaded for a few seconds, so the removal is repeated. What stays is
# reported by the check that follows, found by its folder and not by its user, who is gone by now.
$usersFolder = Join-Path -Path $env:SystemDrive -ChildPath 'Users'
$attempt = 0
do {
$attempt++
@(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.LocalPath -like (Join-Path -Path $usersFolder -ChildPath 'NtfsProbe*') }) | Remove-CimInstance -ErrorAction SilentlyContinue
Get-ChildItem -LiteralPath $usersFolder -Filter 'NtfsProbe*' -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
$left = @(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.LocalPath -like (Join-Path -Path $usersFolder -ChildPath 'NtfsProbe*') }).Count +
@(Get-ChildItem -LiteralPath $usersFolder -Filter 'NtfsProbe*' -Force -ErrorAction SilentlyContinue).Count
if ($left -gt 0 -and $attempt -lt 10) { Start-Sleep -Seconds 3 }
} while ($left -gt 0 -and $attempt -lt 10)
$messages.Add(('stage items, probe folders, probe users, their entries in the log group, and scheduled tasks of the kit removed; profile items left: {0} after {1} attempt(s)' -f $left, $attempt))
$messages
}
foreach ($message in @(Invoke-LabCommand -ComputerName $name -ActivityName "Repair $name" -ScriptBlock $repairScript -ArgumentList (, $sids) @labCommand)) {
'{0,-9} repair: {1}' -f $name, $message
}
}
$state = Invoke-LabCommand -ComputerName $name -ActivityName "Check $name" -ScriptBlock $machineScript -ArgumentList (, $sids) @labCommand
'{0,-9} share={1} C:\NTFSSecurityLive={2} C:\NTFSSecurityLab={3} NtfsLiveLocal={4} profiles={5}' -f $name, $state.Share, $state.ShareRoot, $state.Payload, $state.LocalGroup, $state.Profiles
' {0}' -f $state.Groups
' residue: scheduled tasks={0} stage items={1} probe users={2} probe profiles={3} probe group members={4}' -f $state.Tasks, $state.Stages, $state.Users, $state.ProbeProfiles, $state.ProbeMembers
}
}
'[{0:yyyy-MM-dd HH:mm:ss}Z] matrix-cleanup-{1}-DONE' -f [DateTime]::UtcNow, $Mode
} *>&1 | Out-File -FilePath $OutFile -Encoding utf8 -Width 400

79
Tests/Lab/Acceptance/Test-MatrixReadiness.ps1

@ -0,0 +1,79 @@
[CmdletBinding()]
param (
[string] $LabName = 'NtfsSecurityOsMatrixLab',
[string[]] $DomainController = @('OSDC1'),
[string[]] $Member = @('OSFile19', 'OSFile22', 'OSFile25', 'OSWin11E'),
[Parameter(Mandatory)] [string] $OutFile
)
# Readiness and identity of the machines of an AutomatedLab lab for the live tests of NTFSSecurity, in Windows PowerShell 5.1 on the
# Hyper-V host. It proves what the tests need, not that a VM runs: authenticated WinRM through AutomatedLab, the operating system
# build, the domain, the clock against the host, LDAP and a Kerberos ticket (a domain controller), or the secure channel, the domain
# controller locator and a service ticket for a peer (a member), the PowerShell 7 and Pester payloads, and the ports of SMB, RPC, and
# WinRM from the host. Nothing is changed in the lab. Passwords are never read or printed.
& {
$ErrorActionPreference = 'Stop'
# -File passes an array as one string, so a list may arrive as 'A,B'.
$DomainController = @($DomainController | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$Member = @($Member | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
'[{0:yyyy-MM-dd HH:mm:ss}Z] START matrix-readiness lab={1}' -f [DateTime]::UtcNow, $LabName
Import-Lab -Name $LabName -NoValidation -NoDisplay
$labCommand = @{ NoDisplay = $true; PassThru = $true; ErrorAction = 'Stop' }
$everyMachine = @($DomainController) + @($Member)
$peerByMember = @{}
foreach ($name in $Member) { $peerByMember[$name] = @($Member | Where-Object -FilterScript { $_ -ne $name })[0] }
foreach ($name in $everyMachine) {
$address = (Get-LabVM -ComputerName $name).IpV4Address
$wsman = try { $null = Test-WSMan -ComputerName $address -ErrorAction Stop; 'ok' } catch { "failed: $($_.Exception.Message)" }
$ports = foreach ($port in 135, 445, 5985) {
$client = New-Object -TypeName 'System.Net.Sockets.TcpClient'
try { $open = $client.ConnectAsync($address, $port).Wait(3000) } catch { $open = $false } finally { $client.Dispose() }
'{0}={1}' -f $port, $open
}
$hostUtc = [DateTime]::UtcNow
$state = Invoke-LabCommand -ComputerName $name -ActivityName "Readiness of $name" -ScriptBlock {
param ([bool] $IsDomainController, [string] $Peer)
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$computer = Get-CimInstance -ClassName Win32_ComputerSystem
$version = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$dotNet = (Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -ErrorAction SilentlyContinue).Release
$pwshPath = Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe'
$result = [ordered]@{
Os = '{0} {1}.{2}' -f $os.Caption, $os.Version, $version.UBR
ProductType = $os.ProductType
Edition = $version.EditionID
Domain = $computer.Domain
Utc = [DateTime]::UtcNow
DotNet = $dotNet
WindowsPowerShell = $PSVersionTable.PSVersion.ToString()
PowerShell7 = $(if (Test-Path -LiteralPath $pwshPath) { (Get-Item -LiteralPath $pwshPath).VersionInfo.ProductVersion } else { 'missing' })
PesterDesktop = $(@(Get-Module -Name Pester -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 | ForEach-Object -Process { $_.Version.ToString() }) -join '')
PesterCore = $(if (Test-Path -LiteralPath (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\Modules\Pester\5.7.1\Pester.psd1')) { '5.7.1' } else { 'missing' })
Ldap = ''
Channel = ''
Kerberos = ''
}
if ($IsDomainController) {
$rootDse = [adsi]'LDAP://RootDSE'
$result.Ldap = 'RootDSE {0}, synchronized {1}' -f $rootDse.dnsHostName.Value, $rootDse.isSynchronized.Value
$result.Kerberos = (& klist.exe get "krbtgt/$($computer.Domain)" 2>&1 | Select-String -Pattern 'Error|Server: krbtgt' | Select-Object -First 1).Line
}
else {
$result.Ldap = (& nltest.exe "/dsgetdc:$($computer.Domain)" 2>&1 | Select-String -Pattern '^\s*DC: |ERROR' | Select-Object -First 1).Line
$result.Channel = 'secure channel {0}' -f (Test-ComputerSecureChannel)
$result.Kerberos = (& klist.exe get "host/$Peer.$($computer.Domain)" 2>&1 | Select-String -Pattern 'Error|Server: host' | Select-Object -First 1).Line
}
[pscustomobject] $result
} -ArgumentList ($name -in $DomainController), $peerByMember[$name] @labCommand
$skew = [Math]::Round(($state.Utc - $hostUtc).TotalSeconds, 1)
'{0,-9} wsman={1} ports({2}) os={3} type={4} edition={5} domain={6} skew={7}s' -f $name, $wsman, ($ports -join ' '), $state.Os, $state.ProductType, $state.Edition, $state.Domain, $skew
' .NET release={0}; Windows PowerShell {1}; PowerShell 7 {2}; Pester Desktop {3}, Core {4}' -f $state.DotNet, $state.WindowsPowerShell, $state.PowerShell7, $state.PesterDesktop, $state.PesterCore
' ldap: {0}' -f ("$($state.Ldap)".Trim())
if ($state.Channel) { ' {0}' -f $state.Channel }
' kerberos: {0}' -f ("$($state.Kerberos)".Trim())
}
'[{0:yyyy-MM-dd HH:mm:ss}Z] matrix-readiness-DONE' -f [DateTime]::UtcNow
} *>&1 | Out-File -FilePath $OutFile -Encoding utf8 -Width 400

111
Tests/Lab/Acceptance/Test-PublishedRelease.ps1

@ -0,0 +1,111 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^\d+\.\d+\.\d+(-[0-9A-Za-z]+)?$')] [string] $Version,
[Parameter(Mandatory)] [string] $OutputPath,
[string] $Repository = 'raandree/NTFSSecurity'
)
# Read-only identity check of a published NTFSSecurity version (acceptance of a published candidate): the tag, its commit on master, the CI run of the
# tag, the GitHub release asset, and the PowerShell Gallery package. It downloads the nupkg and the zip into OutputPath, checks the
# SHA-512 that the Gallery publishes (ordinal, case-sensitive base64), extracts both with System.IO.Compression, and compares the
# module files byte for byte. It writes Identity.json and prints a table; it changes nothing on GitHub or in the Gallery, and
# it never imports the module. Exit code 1 for any mismatch.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Add-Type -AssemblyName System.IO.Compression.FileSystem
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
$headers = @{ 'User-Agent' = 'ntfssecurity-published-identity-check'; Accept = 'application/vnd.github+json' }
$api = "https://api.github.com/repos/$Repository"
$problems = New-Object -TypeName 'System.Collections.Generic.List[string]'
$result = [ordered]@{ Version = $Version; CheckedUtc = [DateTime]::UtcNow.ToString('o') }
# 1. The tag and its commit
$ref = Invoke-RestMethod -Uri "$api/git/ref/tags/$Version" -Headers $headers
$sha = $ref.object.sha
if ($ref.object.type -eq 'tag') { $sha = (Invoke-RestMethod -Uri "$api/git/tags/$sha" -Headers $headers).object.sha }
$result.TagCommit = $sha
$compare = Invoke-RestMethod -Uri "$api/compare/master...$sha" -Headers $headers
$result.CommitOnMaster = ($compare.status -in 'identical', 'behind')
$result.CompareStatus = $compare.status
if (-not $result.CommitOnMaster) { $problems.Add("The commit $sha of the tag isn't on master (compare status: $($compare.status)).") }
# 2. The CI run of the tag: the tag push has the tag as its branch name
$runs = @((Invoke-RestMethod -Uri "$api/actions/runs?head_sha=$sha&per_page=30" -Headers $headers).workflow_runs | Where-Object -FilterScript { $_.event -eq 'push' -and $_.head_branch -eq $Version })
if ($runs.Count -eq 0) { $problems.Add("No CI run of the tag push for $Version.") }
$jobs = @()
foreach ($run in ($runs | Sort-Object -Property run_number)) {
$jobs += @((Invoke-RestMethod -Uri "$api/actions/runs/$($run.id)/jobs?per_page=50" -Headers $headers).jobs | ForEach-Object -Process {
[pscustomobject]@{ Run = $run.id; Attempt = $run.run_attempt; Job = $_.name; Status = $_.status; Conclusion = $_.conclusion }
})
}
$result.CiJobs = $jobs
$latestRelease = @($jobs | Where-Object -FilterScript { $_.Job -match 'Release' } | Sort-Object -Property Attempt | Select-Object -Last 1)
if ($latestRelease.Count -eq 0 -or $latestRelease[0].Conclusion -ne 'success') { $problems.Add('The latest Release job of the tag did not succeed.') }
# 3. The GitHub release and its zip
$release = Invoke-RestMethod -Uri "$api/releases/tags/$Version" -Headers $headers
$asset = @($release.assets | Where-Object -FilterScript { $_.name -eq 'NTFSSecurity.zip' }) | Select-Object -First 1
if (-not $asset) { throw "The release $Version has no NTFSSecurity.zip." }
$zipPath = Join-Path -Path $OutputPath -ChildPath "NTFSSecurity-$Version.zip"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing
$zipSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $zipPath).Hash
$result.Release = [ordered]@{ Prerelease = $release.prerelease; Published = $release.published_at; AssetSize = $asset.size; AssetDigest = $asset.digest; ZipSha256 = $zipSha256 }
if ($asset.digest -and $asset.digest -like 'sha256:*' -and ($asset.digest.Substring(7) -ne $zipSha256.ToLowerInvariant())) { $problems.Add('The SHA-256 of the downloaded zip differs from the digest of the release asset.') }
# 4. The PowerShell Gallery package; the published hash is base64 of SHA-512
$entry = Invoke-RestMethod -Uri ("https://www.powershellgallery.com/api/v2/Packages(Id='NTFSSecurity',Version='{0}')" -f $Version)
$published = $entry.entry.properties.PackageHash.'#text'
if (-not $published) { $published = [string] $entry.entry.properties.PackageHash }
$algorithm = $entry.entry.properties.PackageHashAlgorithm
if (-not $published -or $algorithm -ne 'SHA512') { throw "The Gallery has no SHA512 hash for NTFSSecurity $Version (algorithm '$algorithm')." }
$nupkgPath = Join-Path -Path $OutputPath -ChildPath "NTFSSecurity.$Version.nupkg"
Invoke-WebRequest -Uri "https://www.powershellgallery.com/api/v2/package/NTFSSecurity/$Version" -OutFile $nupkgPath -UseBasicParsing
$sha512 = [System.Security.Cryptography.SHA512]::Create()
$stream = [System.IO.File]::OpenRead($nupkgPath)
try { $actual = [Convert]::ToBase64String($sha512.ComputeHash($stream)) } finally { $stream.Dispose(); $sha512.Dispose() }
$hashMatches = [string]::Equals($actual, $published, [StringComparison]::Ordinal)
$result.Gallery = [ordered]@{ Published = $entry.entry.properties.Published.'#text'; IsPrerelease = $entry.entry.properties.IsPrerelease.'#text'; PackageHashAlgorithm = $algorithm; PackageHash = $published; DownloadedSha512 = $actual; HashMatches = $hashMatches; NupkgSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $nupkgPath).Hash }
if (-not $hashMatches) { $problems.Add('The downloaded nupkg does not have the SHA-512 that the Gallery publishes.') }
# 5. The module files of both packages
$nupkgFolder = Join-Path -Path $OutputPath -ChildPath "nupkg-$Version"
$zipFolder = Join-Path -Path $OutputPath -ChildPath "zip-$Version"
foreach ($folder in $nupkgFolder, $zipFolder) { if (Test-Path -LiteralPath $folder) { Remove-Item -LiteralPath $folder -Recurse -Force } }
[System.IO.Compression.ZipFile]::ExtractToDirectory($nupkgPath, $nupkgFolder)
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $zipFolder)
function Get-ModuleRoot { param ([string] $Folder) (Get-ChildItem -LiteralPath $Folder -Filter 'NTFSSecurity.psd1' -Recurse -File | Select-Object -First 1).DirectoryName }
$nupkgRoot = Get-ModuleRoot -Folder $nupkgFolder
$zipRoot = Get-ModuleRoot -Folder $zipFolder
$files = foreach ($file in Get-ChildItem -LiteralPath $zipRoot -Recurse -File) {
$relative = $file.FullName.Substring($zipRoot.Length).TrimStart('\')
$other = Join-Path -Path $nupkgRoot -ChildPath $relative
$zipHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
$nupkgHash = if (Test-Path -LiteralPath $other) { (Get-FileHash -Algorithm SHA256 -LiteralPath $other).Hash } else { '' }
[pscustomobject]@{ File = $relative; ZipSha256 = $zipHash; NupkgSha256 = $nupkgHash; Equal = ($zipHash -eq $nupkgHash) }
}
$files | Export-Csv -LiteralPath (Join-Path -Path $OutputPath -ChildPath "ModuleFiles-$Version.csv") -NoTypeInformation -Encoding utf8
$result.ModuleFiles = @($files).Count
$result.ModuleFilesEqual = (@($files | Where-Object -FilterScript { -not $_.Equal }).Count -eq 0)
$result.ModuleDllSha256 = ($files | Where-Object -FilterScript { $_.File -eq 'NTFSSecurity.dll' }).ZipSha256
if (-not $result.ModuleFilesEqual) { $problems.Add('The module files of the nupkg and of the zip differ.') }
# 6. The identity that the manifest claims
$manifest = Import-PowerShellDataFile -LiteralPath (Join-Path -Path $zipRoot -ChildPath 'NTFSSecurity.psd1')
$label = $manifest.PrivateData.PSData.Prerelease
$claimed = if ($label) { '{0}-{1}' -f $manifest.ModuleVersion, $label } else { [string] $manifest.ModuleVersion }
$result.ManifestVersion = $claimed
if ($claimed -ne $Version) { $problems.Add("The manifest says $claimed, not $Version.") }
$result.Problems = @($problems)
$result.Verified = ($problems.Count -eq 0)
$result | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path -Path $OutputPath -ChildPath "Identity-$Version.json") -Encoding utf8
'Version {0}: tag commit {1}; on master: {2} ({3})' -f $Version, $sha, $result.CommitOnMaster, $compare.status
$jobs | Format-Table -AutoSize | Out-String -Width 200
'GitHub zip SHA-256 {0}' -f $zipSha256
'Gallery SHA-512 matches: {0}; nupkg SHA-256 {1}' -f $hashMatches, $result.Gallery.NupkgSha256
'Module files: {0}; equal in nupkg and zip: {1}; NTFSSecurity.dll SHA-256 {2}' -f $result.ModuleFiles, $result.ModuleFilesEqual, $result.ModuleDllSha256
'Manifest identity: {0}' -f $claimed
if ($problems.Count -gt 0) { $problems | ForEach-Object -Process { 'PROBLEM: ' + $_ }; 'PUBLISHED_IDENTITY_NOT_VERIFIED'; exit 1 }
'PUBLISHED_IDENTITY_VERIFIED'

172
Tests/Lab/Acceptance/Test-StaleAuthzModel.ps1

@ -0,0 +1,172 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $Timeline,
[ValidateRange(1, 60)] [double] $FromMinutes = 3,
[ValidateRange(1, 60)] [double] $ToMinutes = 16,
[ValidateRange(0.01, 5)] [double] $StepMinutes = 0.25,
[ValidateRange(1, 60)] [double] $Lifetime,
[ValidateRange(0, 100000)] [int] $Permutations = 0,
[switch] $AsIfSameSubject,
[switch] $ShowMismatches
)
# Replays the Admin roles of a timeline (Export-CellTimeline.ps1) against a model of the failures that the effective-access tests of the Admin role showed in
# the cells of the operating-system matrix (Decision 24). The model is a description of the observations, not an explanation of Windows:
#
# A remote authorization manager (the one of the client for the default -ServerName, the one of the file server for its own name) computes the groups of an
# account at the first request for the account name and answers from that result for L minutes, also when the account was deleted and created again under
# the same name in the meantime, with a new SID and new group memberships. The answer then has no access through the groups (0x100000, Synchronize only).
#
# For each L from -FromMinutes to -ToMinutes, the script walks the Admin roles in time order, keeps one entry per computer and account name, and predicts
# whether the first (file server) and the second (client) test pass: a test passes when no entry that is younger than L minutes and was made for another
# account instance exists. It reports how many of the observed outcomes each L predicts. A fit says that the position of a cell in the sequence is enough to
# explain the failures, whichever module was under test; it doesn't say how Windows does it, or that the lifetime is a constant. A run whose account name is
# new always passes, which is what the controller relies on since 1dec389. It reads files only; Windows PowerShell 5.1 or PowerShell 7.
#
# -Lifetime L lists every run with the observed and the predicted outcome of both tests for that one L instead of searching for the best L. -AsIfSameSubject
# gives every run the same account name: for the cells of a controller that gives each fixture a new name (rc7l and later), the listing then shows where a
# controller that reuses the name would have met a stale entry. -Permutations N asks how often a random assignment of the observed outcomes to the runs
# (the same number of failures, a fixed random seed) reaches the best agreement of the real outcomes for some L: if the position of a cell decides the
# outcome, it should almost never.
#
# The lifetimes are those of a grid with the step -StepMinutes, so a range is known to within one step (use 0.05 to see the ranges of the record). The model
# doesn't know that a computer restarted. If the state lives in the memory of the computer, a restart would clear it; for the real account names of the series
# of Decision 24, no restart of the client or of a file server changes a prediction (the entry that a restart would have cleared had expired, or the run that
# read it had the same account). For -AsIfSameSubject it matters once: the client restarted between ab6 and ab7.
$ErrorActionPreference = 'Stop'
$random = New-Object -TypeName 'System.Random' -ArgumentList 20261010
$rows = @(Import-Csv -LiteralPath $Timeline | ForEach-Object -Process {
[pscustomobject]@{
Time = [DateTime]::ParseExact($_.AdminRoleStarted, 'yyyy-MM-dd HH:mm:ss', [Globalization.CultureInfo]::InvariantCulture)
Run = $_.Run
Candidate = $_.Candidate
Server = $_.FileServer
Edition = $_.Edition
Subject = if ($AsIfSameSubject) { 'one name' } else { $_.Subject }
Sid = $_.SubjectRid
Test1 = $_.T1ServerNameFileServer -like 'pass*'
Test2 = $_.T2DefaultServerName -like 'pass*'
}
} | Sort-Object -Property Time)
function Test-Model {
param ([double] $Minutes, [ValidateSet('Test1', 'Test2')] [string] $Test)
$entries = @{}
$mismatch = New-Object -TypeName 'System.Collections.Generic.List[string]'
$predictions = New-Object -TypeName 'System.Collections.Generic.List[bool]'
$agree = 0
foreach ($row in $rows) {
$scope = if ($Test -eq 'Test2') { 'client|' + $row.Subject } else { $row.Server + '|' + $row.Subject }
$entry = $entries[$scope]
if ($entry -and ($row.Time - $entry.Created).TotalMinutes -lt $Minutes) {
$predicted = $entry.Sid -eq $row.Sid
}
else {
$entries[$scope] = @{ Created = $row.Time; Sid = $row.Sid }
$predicted = $true
}
$predictions.Add($predicted)
$observed = $row.$Test
if ($predicted -eq $observed) {
$agree++
}
else {
$mismatch.Add(('{0:HH:mm} {1} {2} {3} [{4}]: observed {5}, model {6}' -f $row.Time, $row.Run, $row.Server, $row.Edition, $row.Candidate,
$(if ($observed) { 'pass' } else { 'FAIL' }), $(if ($predicted) { 'pass' } else { 'FAIL' })))
}
}
[pscustomobject]@{ Minutes = $Minutes; Agree = $agree; Mismatch = $mismatch; Predictions = $predictions }
}
if ($PSBoundParameters.ContainsKey('Lifetime')) {
$first = Test-Model -Minutes $Lifetime -Test Test1
$second = Test-Model -Minutes $Lifetime -Test Test2
$word = { param ($Passed) if ($Passed) { 'pass' } else { 'FAIL' } }
for ($index = 0; $index -lt $rows.Count; $index++) {
$row = $rows[$index]
[pscustomobject]@{
Time = $row.Time.ToString('MM-dd HH:mm:ss')
Run = $row.Run
Server = $row.Server
Edition = $row.Edition
Candidate = $row.Candidate
Subject = $row.Subject
Test1 = & $word $row.Test1
Test1Model = & $word $first.Predictions[$index]
Test2 = & $word $row.Test2
Test2Model = & $word $second.Predictions[$index]
}
}
return
}
function Get-Range {
# The lifetimes of a result list as ranges of the grid: 'a to b', or 'a to b and c to d' when the list has a gap.
param ([object[]] $Result)
$segments = New-Object -TypeName 'System.Collections.Generic.List[string]'
$start = $null
$last = $null
foreach ($item in $Result) {
if ($null -eq $start) {
$start = $item.Minutes
}
elseif ($item.Minutes - $last -gt $StepMinutes * 1.5) {
$segments.Add(('{0:N2} to {1:N2}' -f $start, $last))
$start = $item.Minutes
}
$last = $item.Minutes
}
if ($null -ne $start) { $segments.Add(('{0:N2} to {1:N2}' -f $start, $last)) }
$segments -join ' and '
}
# Every lifetime is computed from its index, because adding the step again and again drifts and would lose the last grid point of a range.
$grid = @(for ($step = 0; ; $step++) {
$value = [Math]::Round($FromMinutes + $step * $StepMinutes, 6)
if ($value -gt $ToMinutes) { break }
$value
})
$resultsByTest = @{}
foreach ($test in 'Test1', 'Test2') {
$results = @(foreach ($minutes in $grid) { Test-Model -Minutes $minutes -Test $test })
$resultsByTest[$test] = $results
$best = ($results | Measure-Object -Property Agree -Maximum).Maximum
$bestResults = @($results | Where-Object -FilterScript { $_.Agree -eq $best })
$failures = @($rows | Where-Object -FilterScript { -not $_.$test }).Count
'{0} ({1}): the model predicts {2} of {3} outcomes for L from {4} minutes; {5} runs failed' -f $test,
$(if ($test -eq 'Test1') { 'the name of the file server' } else { 'the default server name, the client' }), $best, $rows.Count, (Get-Range -Result $bestResults), $failures
if ($ShowMismatches) { foreach ($line in $bestResults[0].Mismatch) { ' mismatch: ' + $line } }
if ($Permutations -gt 0) {
$observed = [bool[]] @($rows | ForEach-Object -Process { $_.$test })
$reached = 0
$highest = 0
for ($shuffle = 0; $shuffle -lt $Permutations; $shuffle++) {
$shuffled = [bool[]] @($observed | Sort-Object -Property { $random.Next() })
$agreement = 0
foreach ($result in $results) {
$agree = 0
for ($index = 0; $index -lt $shuffled.Count; $index++) { if ($result.Predictions[$index] -eq $shuffled[$index]) { $agree++ } }
if ($agree -gt $agreement) { $agreement = $agree }
}
if ($agreement -ge $best) { $reached++ }
if ($agreement -gt $highest) { $highest = $agreement }
}
' {0} of {1} random assignments of the outcomes to the runs reach {2} of {3} for some L; the best of them reaches {4}' -f $reached, $Permutations, $best, $rows.Count, $highest
}
}
# One lifetime for both tests: the range of L at which the model predicts the most outcomes of the two tests together.
$together = @(for ($index = 0; $index -lt $grid.Count; $index++) {
[pscustomobject]@{ Minutes = $grid[$index]; Agree = $resultsByTest['Test1'][$index].Agree + $resultsByTest['Test2'][$index].Agree }
})
$bestTogether = ($together | Measure-Object -Property Agree -Maximum).Maximum
'Both tests with one L: the model predicts {0} of {1} outcomes for L from {2} minutes' -f $bestTogether, (2 * $rows.Count), (Get-Range -Result @($together | Where-Object -FilterScript { $_.Agree -eq $bestTogether }))

68
Tests/Lab/Acceptance/Validate-LabResults.ps1

@ -0,0 +1,68 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ResultsFolder,
[Parameter(Mandatory)] [string] $OutputPrefix,
[string[]] $Edition = @('Desktop', 'Core'),
[ValidateSet('Candidate', 'Baseline')] [string] $Expect = 'Candidate'
)
# Validates one controller result folder: every edition and role has exactly one result, and for a candidate no test failed
# and every exit code is 0. It writes the counts per role, every test with its result (from the result files of the roles,
# not from the counts), and the failures with their full names and messages. A Desktop ConvertFrom-Json wraps an array in
# one object, so each JSON array is enumerated explicitly. -File passes an array as one string.
$ErrorActionPreference = 'Stop'
$Edition = @($Edition | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$summary = @(Get-Content -LiteralPath (Join-Path -Path $ResultsFolder -ChildPath 'Summary.json') -Raw | ConvertFrom-Json | ForEach-Object -Process { $_ })
$roles = 'Delegate', 'ServerAdmin', 'Admin', 'Server'
$expected = @(foreach ($name in $Edition) { foreach ($role in $roles) { '{0}:{1}' -f $name, $role } })
$actual = @($summary | ForEach-Object -Process { '{0}:{1}' -f $_.Edition, $_.Role })
$problems = New-Object -TypeName 'System.Collections.Generic.List[string]'
foreach ($identity in $expected) {
$count = @($actual | Where-Object -FilterScript { $_ -eq $identity }).Count
if ($count -ne 1) { $problems.Add("$identity has $count results instead of 1") }
}
if ($summary.Count -ne $expected.Count) { $problems.Add("The summary has $($summary.Count) results instead of $($expected.Count)") }
$counts = foreach ($entry in $summary) {
[pscustomobject]@{
Version = $entry.Version; Edition = $entry.Edition; Role = $entry.Role; Account = $entry.Account; ExitCode = $entry.ExitCode
Passed = $entry.Passed; Failed = $entry.Failed; Skipped = $entry.Skipped
}
}
$tests = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ($file in Get-ChildItem -LiteralPath $ResultsFolder -Filter '*.result.json') {
$baseName = $file.Name -replace '\.result\.json$', ''
$role = ($baseName -split '-')[-1]
$resultEdition = if ($baseName -match '-(Desktop|Core)-') { $Matches[1] } else { '' }
foreach ($case in @(Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json | ForEach-Object -Process { $_ })) {
$tests.Add([pscustomobject]@{ Edition = $resultEdition; Role = $role; Test = $case.Name; Result = $case.Result; Message = (($case.Message -split '\r?\n')[0]) })
}
}
$failures = @($tests | Where-Object -FilterScript { $_.Result -eq 'Failed' })
if ($Expect -eq 'Candidate') {
foreach ($row in $counts) {
if ($row.ExitCode -ne 0 -or $row.Failed -ne 0 -or $row.Passed -eq 0) { $problems.Add("$($row.Edition) $($row.Role): exit code $($row.ExitCode), $($row.Passed) passed, $($row.Failed) failed") }
}
if ($failures.Count -gt 0) { $problems.Add("$($failures.Count) failed tests in the result files") }
}
$counts | Export-Csv -LiteralPath ($OutputPrefix + '-counts.csv') -NoTypeInformation -Encoding utf8
$tests | Export-Csv -LiteralPath ($OutputPrefix + '-tests.csv') -NoTypeInformation -Encoding utf8
$failures | Export-Csv -LiteralPath ($OutputPrefix + '-failures.csv') -NoTypeInformation -Encoding utf8
foreach ($editionName in $Edition) {
$selected = @($counts | Where-Object -FilterScript { $_.Edition -eq $editionName })
'{0}: passed={1}, failed={2}, skipped={3}' -f $editionName, ($selected.Passed | Measure-Object -Sum).Sum, ($selected.Failed | Measure-Object -Sum).Sum, ($selected.Skipped | Measure-Object -Sum).Sum
}
$counts | Format-Table -AutoSize | Out-String -Width 200
'tests in the result files: {0}; failed: {1}; skipped: {2}' -f $tests.Count, $failures.Count, @($tests | Where-Object -FilterScript { $_.Result -eq 'Skipped' }).Count
if ($problems.Count -gt 0) {
$problems | ForEach-Object -Process { 'PROBLEM: ' + $_ }
'LIVE_RESULT_NOT_ACCEPTED'
exit 1
}
'LIVE_RESULT_VERIFIED ({0})' -f $Expect

137
Tests/Lab/Invoke-NTFSSecurityLabTest.ps1

@ -128,15 +128,16 @@ $roleAccounts = [ordered]@{
ServerAdmin = 'NtfsLiveServerAdmin'
Admin = 'NtfsLiveAdmin'
}
$subjectAccount = 'NtfsLiveSubject'
$subjectBaseName = 'NtfsLiveSubject'
$orphanAccount = 'NtfsLiveOrphan'
$foreignAccount = 'NtfsLiveForeign'
# The rights that the entries of the foreign accounts grant on the folder of case 9, by position
$foreignRights = 'ReadAndExecute', 'Modify', 'Write'
$localGroupName = 'NtfsLiveLocal'
# The members of NtfsLiveInner follow when the name of the account of case 3 is known
$groupMembers = @{
NtfsLiveDelegates = @('NtfsLiveDelegate')
NtfsLiveInner = @('NtfsLiveSubject')
NtfsLiveInner = @()
NtfsLiveOuter = @('NtfsLiveInner')
}
# A name that no DNS server resolves (RFC 2606)
@ -300,6 +301,19 @@ function ConvertFrom-LabTestResult {
}
#region Remote script blocks
# Runs on the domain controller: returns the names of the accounts of case 3 that the organizational unit already has.
$findSubjectScript = {
param ($OrganizationalUnitName, $BaseName)
$ErrorActionPreference = 'Stop'
Import-Module -Name ActiveDirectory
$domain = Get-ADDomain
$path = 'OU={0},{1}' -f $OrganizationalUnitName, $domain.DistinguishedName
if (Get-ADOrganizationalUnit -LDAPFilter "(ou=$OrganizationalUnitName)" -SearchBase $domain.DistinguishedName -SearchScope OneLevel -Server $domain.PDCEmulator) {
Get-ADUser -LDAPFilter "(sAMAccountName=$BaseName*)" -SearchBase $path -Server $domain.PDCEmulator | ForEach-Object -Process { $_.SamAccountName }
}
}
# Runs on the domain controller: creates or updates the accounts and groups in their organizational unit, pushes them
# to the other domain controllers of the domain, and returns their SIDs.
$accountScript = {
@ -466,25 +480,51 @@ $fileServerSetupScript = {
$null = New-LocalGroup -Name $LocalGroupName -Description 'NTFSSecurity live tests'
}
if ($SubjectSid -notin @(Get-LocalGroupMember -Name $LocalGroupName | ForEach-Object -Process { $_.SID.Value })) {
# Add the members and ignore the error for a member that exists. A check with Get-LocalGroupMember would fail with "Failed to
# compare two elements in the array" in Windows PowerShell 5.1 as soon as the group holds an orphaned SID, for example that of
# an account that an earlier run deleted.
try {
Add-LocalGroupMember -Name $LocalGroupName -Member $SubjectSid
}
catch {
if ($_.Exception.GetType().Name -ne 'MemberExistsException') {
throw
}
}
foreach ($sid in $AdministratorSid) {
if ($sid -notin @(Get-LocalGroupMember -SID 'S-1-5-32-544' | ForEach-Object -Process { $_.SID.Value })) {
try {
Add-LocalGroupMember -SID 'S-1-5-32-544' -Member $sid
}
catch {
if ($_.Exception.GetType().Name -ne 'MemberExistsException') {
throw
}
}
}
if (-not (Test-Path -LiteralPath $ShareLocalPath)) {
$null = New-Item -ItemType Directory -Path $ShareLocalPath
}
# The folders of earlier runs, also those with paths longer than 260 characters, which PowerShell 7 removes.
$command = '$ErrorActionPreference = ''Stop''; Get-ChildItem -LiteralPath ''{0}'' -Force | Remove-Item -Recurse -Force' -f $ShareLocalPath
$output = & (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe') -NoProfile -NonInteractive -EncodedCommand ([Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))) 2>&1
if ($LASTEXITCODE -ne 0) {
throw "The folders of earlier runs could not be removed: $output"
# The folders of earlier runs, also those with paths longer than 260 characters, which PowerShell 7 removes. A recursive removal can
# fail with "The directory is not empty" while another process, such as a virus scanner, still holds a handle to an item that was
# just deleted (seen on Windows Server 2019), so it is repeated. The command writes its errors to its output: a line on stderr would
# end this script at once, because the error action here is Stop and 2>&1 turns that line into a terminating error.
$command = '$errors = @(); Get-ChildItem -LiteralPath ''__PATH__'' -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable errors; $errors | ForEach-Object -Process { "$_" }'.Replace('__PATH__', $ShareLocalPath)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
$attempt = 0
do {
$attempt++
if ($attempt -gt 1) {
Start-Sleep -Seconds 5
}
$output = & (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe') -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1
} while (@(Get-ChildItem -LiteralPath $ShareLocalPath -Force -ErrorAction SilentlyContinue).Count -gt 0 -and $attempt -lt 6)
if (@(Get-ChildItem -LiteralPath $ShareLocalPath -Force -ErrorAction SilentlyContinue).Count -gt 0) {
throw "The folders of earlier runs could not be removed in $attempt attempts: $output"
}
# Administrators and the system own the share; the delegated group may read it, and the folders of the cases grant
@ -524,9 +564,15 @@ $clientSetupScript = {
@{ Group = 'S-1-5-32-580'; Members = $RemoteUserSid }
)) {
foreach ($sid in $membership.Members) {
if ($sid -notin @(Get-LocalGroupMember -SID $membership.Group | ForEach-Object -Process { $_.SID.Value })) {
# See the file server setup: Get-LocalGroupMember fails on an orphaned SID.
try {
Add-LocalGroupMember -SID $membership.Group -Member $sid
}
catch {
if ($_.Exception.GetType().Name -ne 'MemberExistsException') {
throw
}
}
}
}
@ -723,6 +769,27 @@ $fixtureScript = {
}
)
# Case 10: the behavior that the fixes of the quality gate before 5.0.0 changed. The tests create their items below the
# folder of their role, which the delegated group fully controls. Administrators own the folder Locked, whose
# permissions the delegated account denies itself, and the files that Set-NTFSOwner changes: a file that the account
# created would be owned by the account already.
$null = New-FixtureFolder -RelativePath 'Case10' -AccessRule $delegatesFullControl
$null = New-FixtureFolder -RelativePath 'Case10\Locked'
foreach ($role in 'Admin', 'ServerAdmin', 'Delegate') {
foreach ($style in 'Select', 'Throw') {
foreach ($ownerFolder in "SetOwner-$style", "SetOwner-Debug$style") {
$ownerPath = New-FixtureFolder -RelativePath "Case10\$role\LaterCommand\$ownerFolder"
foreach ($name in 'First', 'Second') {
$file = Join-Path -Path $ownerPath -ChildPath "$name.txt"
Set-Content -LiteralPath $file -Value $name -NoNewline
if ((Get-LabSecurityDescriptor -Path $file).Owner.Value -ne 'S-1-5-32-544') {
throw "Administrators don't own '$file'."
}
}
}
}
}
# The rights that the file server's own token of each foreign account gets on the folder, like case 3. A token
# that the file server can't create is reported as -1, which fails only the effective-access test of the account.
$foreignDescriptor = Get-LabSecurityDescriptor -Path $foreignPath
@ -844,12 +911,28 @@ $removeFileServerScript = {
}
foreach ($path in $ShareLocalPath, $PayloadPath) {
# PowerShell 7 removes the symbolic links of the tests without following them, which Windows PowerShell 5.1 doesn't do. A recursive
# removal can still fail with "The directory is not empty" while another process, such as a virus scanner, holds a handle to an item
# that was just deleted (seen on Windows Server 2019); a moment later nothing is left. So the removal is repeated before it fails.
# The command writes its errors to its output: a line on stderr would end this script at once (see the setup of the file server).
$command = '$errors = @(); Remove-Item -LiteralPath ''__PATH__'' -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable errors; $errors | ForEach-Object -Process { "$_" }'.Replace('__PATH__', $path)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
$attempt = 0
while ((Test-Path -LiteralPath $path) -and $attempt -lt 6) {
$attempt++
if ($attempt -gt 1) {
Start-Sleep -Seconds 5
}
$output = & (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe') -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1
}
if (Test-Path -LiteralPath $path) {
$command = '$ErrorActionPreference = ''Stop''; Remove-Item -LiteralPath ''{0}'' -Recurse -Force' -f $path
$output = & (Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\7\pwsh.exe') -NoProfile -NonInteractive -EncodedCommand ([Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))) 2>&1
if ($LASTEXITCODE -ne 0) {
throw "'$path' could not be removed: $output"
throw "'$path' could not be removed in $attempt attempts: $output"
}
if ($attempt -gt 1) {
"'$path' was removed in $attempt attempts."
}
}
@ -872,7 +955,18 @@ $removeClientScript = {
}
}
Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.SID -in $Sid } | Remove-CimInstance
# A profile that is gone in the meantime needs no removal; one that is still there after the error does.
foreach ($userProfile in @(Get-CimInstance -ClassName Win32_UserProfile | Where-Object -FilterScript { $_.SID -in $Sid })) {
try {
Remove-CimInstance -InputObject $userProfile
}
catch {
if (Get-CimInstance -ClassName Win32_UserProfile -Filter ("SID = '{0}'" -f $userProfile.SID) -ErrorAction SilentlyContinue) {
throw
}
}
}
if (Test-Path -LiteralPath $PayloadPath) {
Remove-Item -LiteralPath $PayloadPath -Recurse -Force
}
@ -934,7 +1028,10 @@ if ($RemoveFixture) {
}
$accountSids = @(Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Read the accounts' -ScriptBlock $accountSidScript -ArgumentList $organizationalUnitName @labCommand)
$null = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Remove the share and the folders' -ScriptBlock $removeFileServerScript -ArgumentList $shareName, $shareLocalPath, $payloadPath, $localGroupName, $accountSids @labCommand
$removed = Invoke-LabCommand -ComputerName $FileServer -ActivityName 'Remove the share and the folders' -ScriptBlock $removeFileServerScript -ArgumentList $shareName, $shareLocalPath, $payloadPath, $localGroupName, $accountSids @labCommand
foreach ($message in @($removed)) {
Write-LabProgress "${FileServer}: $message"
}
$null = Invoke-LabCommand -ComputerName $Client -ActivityName 'Remove the members and the folder' -ScriptBlock $removeClientScript -ArgumentList $payloadPath, $accountSids @labCommand
$null = Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Remove the accounts' -ScriptBlock $removeAccountScript -ArgumentList $organizationalUnitName @labCommand
foreach ($computer in $ForeignDomainController) {
@ -977,6 +1074,14 @@ $modules = @(
)
Write-LabProgress 'Preparing the accounts, the file server, and the client'
# When an account is deleted and created again with the same name, the remote authorization managers of the client and of the file server, which
# Get-NTFSEffectiveAccess asks for its default -ServerName and for the name of the file server, keep answering for about ten minutes as if the new
# account had no groups (Synchronize only), for the baseline and for the final candidate alike. The local manager and a Kerberos S4U logon of the account, which
# the oracle uses, are right at that moment (Decision 24). So a new fixture gets a name for the account of case 3 that an earlier fixture is unlikely
# to have used (four random digits); a fixture that exists keeps its account.
$existingSubjects = @(Invoke-LabCommand -ComputerName $DomainController -ActivityName 'Look for the account of case 3' -ScriptBlock $findSubjectScript -ArgumentList $organizationalUnitName, $subjectBaseName @labCommand)
$subjectAccount = if ($existingSubjects) { [string]$existingSubjects[0] } else { '{0}{1:D4}' -f $subjectBaseName, (Get-Random -Minimum 0 -Maximum 10000) }
$groupMembers['NtfsLiveInner'] = @($subjectAccount)
$passwords = @{}
foreach ($name in @($roleAccounts.Values) + $subjectAccount) {
$passwords[$name] = New-LabPassword

623
Tests/Lab/NTFSSecurity.Live.Tests.ps1

@ -104,6 +104,31 @@ BeforeDiscovery {
}
}
)
# Case 10: the cmdlets that a later command in the pipeline stops, and the roles whose items the file server checks.
$laterCommandCases = @(
foreach ($name in 'Remove-Item2', 'Copy-Item2', 'Move-Item2', 'Set-NTFSOwner', 'Set-NTFSSecurityDescriptor') {
foreach ($style in 'Select-Object -First 1', 'throw') {
@{ Name = $name; Style = $style }
}
}
)
$laterCommandStreamCases = @(
foreach ($case in @(
@{ Name = 'Set-NTFSSecurityDescriptor'; Stream = 'verbose' }
@{ Name = 'Get-FileHash2'; Stream = 'verbose' }
@{ Name = 'Set-NTFSOwner'; Stream = 'debug' }
)) {
foreach ($style in 'Select-Object -First 1', 'throw') {
@{ Name = $case.Name; Stream = $case.Stream; Style = $style }
}
}
)
$laterCommandStates = @(
foreach ($stateRole in 'Admin', 'ServerAdmin', 'Delegate') {
@{ StateRole = $stateRole }
}
)
}
BeforeAll {
@ -438,6 +463,36 @@ Describe 'Get-NTFSEffectiveAccess as an account that is not an administrator of
}
}
Describe 'Get-NTFSEffectiveAccess for a domain account as an account that is not an administrator of the client' -Tag 'ServerAdmin' -Skip:(-not $configured) {
# The remote authorization manager of a computer answers only its administrators and the members of its group Access
# Control Assistance Operators, and a computer in a domain offers it to every caller. Before 5.0.0-rc7, the cmdlet
# wrote "Access is denied" for this computer, too, so the default -ServerName (localhost) failed for every user who
# isn't an administrator of the client. Now the local authorization manager of the client answers, which is the
# manager that the name asks for.
BeforeAll {
$path = Get-LabPath -RelativePath 'Case3\EffectiveAccess'
$subject = $configuration.Accounts.Subject.Name
}
It 'Should return the rights through the domain groups without -ServerName, like for an administrator of the client' {
$result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights)
}
It 'Should return the same rights without a warning for the name of the client' {
$result = @(Get-NTFSEffectiveAccess -Path $path -Account $subject -ServerName $env:COMPUTERNAME -WarningVariable operationWarnings -WarningAction SilentlyContinue -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
# The warning that the account doesn't hold the Security privilege is allowed; a warning about an unreachable computer isn't.
($operationWarnings.Message -join '|') | Should -Not -BeLike '*can''t be reached*'
$result | Should -HaveCount 1
Format-LabRight -Right $result[0].AccessRights | Should -Be (Format-LabRight -Right $configuration.EffectiveAccess.ClientRights)
}
}
Describe 'Get-NTFSOrphanedAccess with the entry of a deleted domain account on a share folder' -Tag 'Admin' -Skip:(-not $configured) {
BeforeAll {
$folder = Get-LabPath -RelativePath 'Case4\OrphanedAccess'
@ -846,6 +901,538 @@ Describe 'Accounts of another domain and of other forests on share folders' -Tag
}
}
# Case 10: the behavior that the fixes of the quality gate before 5.0.0 changed. Each test works in a folder of its role below
# Case10, which the delegated group fully controls, and fails on a build before the fix that its comment names.
Describe 'An item that the account owns and whose owner may not change its permissions on a share' -Tag 'Delegate' -Skip:(-not $configured) {
# The delegated account owns what it creates on the share. A deny entry for OWNER RIGHTS replaces the right of the owner to
# change the DACL, so the cmdlets take ownership for the write, which the file server answers by removing that entry. Once
# the DACL is cleared and protected, nobody holds the right to set an owner. Before 5.0.0, the cmdlets set the previous
# owner back also when it was the account itself: the file server refused it, and they reported a RestoreOwnerError for an
# owner that had not changed.
BeforeAll {
$folder = Get-LabPath -RelativePath "Case10\$Role\Owner"
$null = New-Item -ItemType Directory -Path $folder -Force
$ownerRights = 'S-1-3-4'
$protectedFlag = [System.Security.AccessControl.ControlFlags]::DiscretionaryAclProtected
function New-LabUnchangeableFile {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.'
)]
param ([string] $Name)
$file = Join-Path -Path $folder -ChildPath $Name
Set-Content -LiteralPath $file -Value $Name -NoNewline
Get-LabOwner -Path $file | Should -Be $configuration.Accounts.$Role.Sid -Because 'the account owns what it creates'
Add-NTFSAccess -Path $file -Account $ownerRights -AccessType Deny -AccessRights ChangePermissions -ErrorAction Stop
# icacls reports the refusal on its error stream, which a terminating error action would turn into an exception
$savedPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$null = & icacls.exe $file /grant '*S-1-1-0:(R)' 2>&1
$exitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $savedPreference
}
$exitCode | Should -Not -Be 0 -Because 'a plain write of the DACL fails for the owner now'
$file
}
function Assert-LabClearedDescriptor {
param ([string] $File)
$descriptor = Get-LabSecurityDescriptor -Path $File
$descriptor.Owner.Value | Should -Be $configuration.Accounts.$Role.Sid
($descriptor.ControlFlags -band $protectedFlag) | Should -Be $protectedFlag
$null -ne $descriptor.DiscretionaryAcl | Should -BeTrue -Because 'the DACL is empty, not NULL'
$descriptor.DiscretionaryAcl.Count | Should -Be 0
}
}
It 'Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError' {
$file = New-LabUnchangeableFile -Name 'Clear.txt'
Clear-NTFSAccess -Path $file -DisableInheritance -ErrorVariable operationErrors -ErrorAction SilentlyContinue
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
Assert-LabClearedDescriptor -File $file
}
It 'Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError' {
$file = New-LabUnchangeableFile -Name 'Descriptor.txt'
$descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
Clear-NTFSAccess -SecurityDescriptor $descriptor -DisableInheritance -ErrorAction Stop
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptor -ErrorVariable operationErrors -ErrorAction SilentlyContinue
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
Assert-LabClearedDescriptor -File $file
}
}
# Windows can't name the folders that the inherited entries of an item come from when the item is gone, for example deleted by
# another process after its security descriptor was read, or when a folder above it can't be read. The entries still come
# back. Before 5.0.0, the text lost its last character, and an explicit entry, which has no source, got it as well.
Describe 'InheritedFrom of access entries that Windows cannot resolve on a share' -Tag 'Delegate', 'ServerAdmin', 'Admin' -Skip:(-not $configured) {
BeforeAll {
$folder = Get-LabPath -RelativePath "Case10\$Role\InheritedFrom"
$null = New-Item -ItemType Directory -Path $folder -Force
}
It 'Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone' {
$file = Join-Path -Path $folder -ChildPath 'Gone.txt'
Set-Content -LiteralPath $file -Value 'Gone' -NoNewline
Add-NTFSAccess -Path $file -Account $everyone -AccessRights ReadData -ErrorAction Stop
$descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
Remove-Item -LiteralPath $file -Force
$entries = @([Security2.FileSystemAccessRule2]::GetFileSystemAccessRules($descriptor, $true, $true, $true))
$inherited = @($entries | Where-Object -FilterScript { $_.IsInherited })
$inherited | Should -Not -BeNullOrEmpty
foreach ($entry in $inherited) {
$entry.InheritedFrom | Should -BeExactly 'unknown parent'
}
$explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited })
$explicit | Should -HaveCount 1
$explicit[0].InheritedFrom | Should -BeNullOrEmpty
}
}
Describe 'InheritedFrom of audit entries that Windows cannot resolve on a share' -Tag 'ServerAdmin', 'Admin' -Skip:(-not $configured) {
# The administrators of the file server read and change the audit entries over SMB (case 2).
BeforeAll {
$folder = Get-LabPath -RelativePath "Case10\$Role\InheritedFromAudit"
$null = New-Item -ItemType Directory -Path $folder -Force
Add-NTFSAudit -Path $folder -Account $everyone -AccessRights ReadData -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None -ErrorAction Stop
}
It 'Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone' {
$file = Join-Path -Path $folder -ChildPath 'Gone.txt'
Set-Content -LiteralPath $file -Value 'Gone' -NoNewline
Add-NTFSAudit -Path $file -Account 'S-1-5-32-546' -AccessRights Delete -InheritanceFlags None -PropagationFlags None -ErrorAction Stop
$descriptor = Get-NTFSSecurityDescriptor -Path $file -ErrorAction Stop
Remove-Item -LiteralPath $file -Force
$entries = @([Security2.FileSystemAuditRule2]::GetFileSystemAuditRules($descriptor, $true, $true, $true))
$inherited = @($entries | Where-Object -FilterScript { $_.IsInherited })
$inherited | Should -HaveCount 1
$inherited[0].InheritedFrom | Should -BeExactly 'unknown parent'
$explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited })
$explicit | Should -HaveCount 1
$explicit[0].InheritedFrom | Should -BeNullOrEmpty
}
}
Describe 'InheritedFrom of an item below a folder on a share whose permissions the account cannot read' -Tag 'Delegate' -Skip:(-not $configured) {
# Administrators own the folder, so a deny entry for the delegated account takes effect for it. The entry applies to the
# folder only: the item below it keeps its entries and stays readable.
BeforeAll {
$locked = Get-LabPath -RelativePath 'Case10\Locked'
$child = Join-Path -Path $locked -ChildPath 'Child.txt'
Set-Content -LiteralPath $child -Value 'Child' -NoNewline
Add-NTFSAccess -Path $child -Account $everyone -AccessRights ReadData -ErrorAction Stop
Add-NTFSAccess -Path $locked -Account $configuration.Accounts.Delegate.Sid -AccessType Deny -AccessRights ReadPermissions -AppliesTo ThisFolderOnly -ErrorAction Stop
}
It 'Should start with a folder whose permissions the account cannot read, and an item that it can' {
{ Get-Acl -LiteralPath $locked -ErrorAction Stop } | Should -Throw
{ Get-Acl -LiteralPath $child -ErrorAction Stop } | Should -Not -Throw
}
It 'Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry' {
$entries = @(Get-NTFSAccess -Path $child -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$inherited = @($entries | Where-Object -FilterScript { $_.IsInherited })
$inherited | Should -Not -BeNullOrEmpty
foreach ($entry in $inherited) {
$entry.InheritedFrom | Should -BeExactly 'unknown parent'
}
$explicit = @($entries | Where-Object -FilterScript { -not $_.IsInherited -and $_.Account.Sid -eq $everyone })
$explicit | Should -HaveCount 1
$explicit[0].InheritedFrom | Should -BeNullOrEmpty
}
}
# Before 5.0.0, a cmdlet took what a later command ended the pipeline with (Select-Object -First, a break) or threw for a failure
# of the item and went on with the next item: Remove-Item2 removed every item after Select-Object -First 1, and the caller
# never saw a throw. Each case runs one command over two items and the file server checks the items afterwards (role Server).
Describe 'A later command that ends the pipeline or throws, for the item cmdlets on a share' -Tag 'Delegate', 'ServerAdmin', 'Admin' -Skip:(-not $configured) {
BeforeAll {
$account = $configuration.Accounts.$Role.Sid
$caseRoot = Get-LabPath -RelativePath "Case10\$Role\LaterCommand"
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
$savedEnablePrivileges = $privateData['EnablePrivileges']
function Get-LabSlug {
param ([string] $Style)
if ($Style -eq 'throw') { 'Throw' } else { 'Select' }
}
function New-LabCaseFolder {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.'
)]
param ([string] $Name)
$path = Join-Path -Path $caseRoot -ChildPath $Name
$null = New-Item -ItemType Directory -Path $path -Force
$path
}
function New-LabPair {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Test helper that only writes to the folder of the run.'
)]
param ([string] $Name)
$directory = New-LabCaseFolder -Name $Name
foreach ($item in 'First', 'Second') {
Set-Content -LiteralPath (Join-Path -Path $directory -ChildPath "$item.txt") -Value $item -NoNewline
}
@{
Directory = $directory
First = (Join-Path -Path $directory -ChildPath 'First.txt')
Second = (Join-Path -Path $directory -ChildPath 'Second.txt')
}
}
# Each case runs one command over the two items of its context. Untouched tells whether the second item is as it was,
# which it is only when the command stopped after the first one.
$cases = @{
'Remove-Item2' = @{
Prepare = { param ($Slug) New-LabPair -Name "RemoveItem2-$Slug" }
Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Copy-Item2' = @{
Prepare = {
param ($Slug)
$context = New-LabPair -Name "CopyItem2-$Slug"
$context.Destination = New-LabCaseFolder -Name "CopyItem2-$Slug-To"
$context
}
Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath 'Second.txt')) }
}
'Move-Item2' = @{
Prepare = {
param ($Slug)
$context = New-LabPair -Name "MoveItem2-$Slug"
$context.Destination = New-LabCaseFolder -Name "MoveItem2-$Slug-To"
$context
}
Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
# The files of the fixture are owned by Administrators, so that the first one changes its owner.
'Set-NTFSOwner' = @{
Prepare = {
param ($Slug)
$directory = Join-Path -Path $caseRoot -ChildPath "SetOwner-$Slug"
@{ First = (Join-Path -Path $directory -ChildPath 'First.txt'); Second = (Join-Path -Path $directory -ChildPath 'Second.txt') }
}
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) (Get-LabOwner -Path $Context.Second) -eq $administrators }
}
'Set-NTFSSecurityDescriptor' = @{
Prepare = {
param ($Slug)
$context = New-LabPair -Name "SetDescriptor-$Slug"
$context.Descriptors = @(Get-NTFSSecurityDescriptor -Path $context.First, $context.Second -ErrorAction Stop)
Add-NTFSAccess -SecurityDescriptor $context.Descriptors -Account $everyone -AccessRights ReadData -ErrorAction Stop
$context
}
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (Get-LabExplicitAccessRule -Path $Context.Second -Sid $everyone) }
}
}
# The command writes a verbose or a debug message inside the try of its loop, which the later command takes. With the
# privileges enabled, the cmdlet writes a message before that, outside the try, so the module setting is off for these
# cases. Get-FileHash2 skips the folder that comes first with a verbose message.
$streamCases = @{
'Set-NTFSSecurityDescriptor/verbose' = @{
Prepare = $cases['Set-NTFSSecurityDescriptor'].Prepare
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -Verbose -ErrorAction SilentlyContinue 4>&1 }
Untouched = $cases['Set-NTFSSecurityDescriptor'].Untouched
RecordType = [System.Management.Automation.VerboseRecord]
}
'Get-FileHash2/verbose' = @{
Prepare = {
param ($Slug)
@{
First = (New-LabCaseFolder -Name "FileHash2-$Slug-Folder")
File = (New-LabPair -Name "FileHash2-$Slug").First
}
}
Run = { param ($Context) Get-FileHash2 -Path $Context.First, $Context.File -Verbose -ErrorAction SilentlyContinue 4>&1 }
RecordType = [System.Management.Automation.VerboseRecord]
}
'Set-NTFSOwner/debug' = @{
Prepare = $cases['Set-NTFSOwner'].Prepare
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -ErrorAction SilentlyContinue 5>&1 }
Untouched = $cases['Set-NTFSOwner'].Untouched
RecordType = [System.Management.Automation.DebugRecord]
}
}
function Assert-LabPipelineStop {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$Error.Clear()
$result = @(& $Case.Run $context | Select-Object -First 1)
$result | Should -HaveCount 1
if ($Case.RecordType) {
$result[0] | Should -BeOfType $Case.RecordType
}
$Error.Count | Should -Be 0
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
# A later command that throws ends the pipeline for the commands before it. The error is the caller's: the cmdlet must
# neither report it as an error of an item nor go on with the next item.
function Assert-LabDownstreamFailure {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$emitted = 0
$caught = $null
$Error.Clear()
try {
& $Case.Run $context | ForEach-Object -Process {
$emitted++
throw 'Downstream failure'
}
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
$emitted | Should -Be 1
@($Error | Where-Object -FilterScript { $_.Exception.Message -notlike '*Downstream failure*' }) | Should -BeNullOrEmpty
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
}
It '<Name> should stop after the first object for <Style> and change nothing else' -ForEach $laterCommandCases {
$slug = Get-LabSlug -Style $Style
if ($Style -eq 'throw') {
Assert-LabDownstreamFailure -Case $cases[$Name] -Slug $slug
}
else {
Assert-LabPipelineStop -Case $cases[$Name] -Slug $slug
}
}
Context 'With the messages of the verbose and debug streams' {
BeforeAll {
$privateData['EnablePrivileges'] = $false
}
AfterAll {
$privateData['EnablePrivileges'] = $savedEnablePrivileges
}
It '<Name> should stop at the <Stream> message for <Style> and change nothing else' -ForEach $laterCommandStreamCases {
$slug = '{0}{1}' -f [System.Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($Stream), (Get-LabSlug -Style $Style)
if ($Style -eq 'throw') {
Assert-LabDownstreamFailure -Case $streamCases["$Name/$Stream"] -Slug $slug -Stream $Stream
}
else {
Assert-LabPipelineStop -Case $streamCases["$Name/$Stream"] -Slug $slug -Stream $Stream
}
}
}
}
# The errors that Get-ChildItem2 writes for a folder that it cannot read reach a later command too, for example with 2>&1.
# Before 5.0.0, the recursion took what the later command threw for a failure of the folder above, and ended the listing.
Describe 'A later command and the error of a folder that Get-ChildItem2 cannot read on a share' -Tag 'Delegate' -Skip:(-not $configured) {
BeforeAll {
$errorTree = Get-LabPath -RelativePath "Case10\$Role\ErrorTree"
$null = New-Item -ItemType Directory -Path $errorTree -Force
$unreadable = foreach ($name in 'A', 'B') {
$path = Join-Path -Path $errorTree -ChildPath $name
$null = New-Item -ItemType Directory -Path $path
# An entry for Everyone stops the delegated account, which isn't the file server's administrator
Add-NTFSAccess -Path $path -Account $everyone -AccessType Deny -AccessRights ReadData -ErrorAction Stop
$path
}
$readableFile = Join-Path -Path $errorTree -ChildPath 'C\Three.txt'
$null = New-Item -ItemType Directory -Path (Split-Path -Path $readableFile -Parent)
Set-Content -LiteralPath $readableFile -Value 'Three' -NoNewline
}
It 'Should start with folders that the account cannot list' {
foreach ($path in $unreadable) {
{ Get-ChildItem -LiteralPath $path -ErrorAction Stop } | Should -Throw
}
}
It 'Should pass on what a later command throws when it takes the error of a nested folder' {
$emitted = 0
$caught = $null
try {
Get-ChildItem2 -Path $errorTree -Recurse -File -ErrorAction Continue 2>&1 | ForEach-Object -Process {
$emitted++
throw 'Downstream failure'
}
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
$emitted | Should -Be 1
}
It 'Should leave the loop for a break of a later command that takes the error of a nested folder' {
$emitted = 0
$reachedEnd = $false
foreach ($round in 1) {
Get-ChildItem2 -Path $errorTree -Recurse -File -ErrorAction Continue 2>&1 | ForEach-Object -Process {
$emitted++
break
}
$reachedEnd = $true
}
$emitted | Should -Be 1
$reachedEnd | Should -BeFalse
}
}
# Only * and ? are wildcards in -Filter, and the pattern *.* selects every item, as it does for Windows and Get-ChildItem.
Describe 'Get-ChildItem2 -Filter on a share folder' -Tag 'Delegate', 'ServerAdmin', 'Admin' -Skip:(-not $configured) {
BeforeAll {
$folder = Get-LabPath -RelativePath "Case10\$Role\Filter"
$null = New-Item -ItemType Directory -Path $folder -Force
$names = 'Report[1].txt', 'Report1.txt', 'Page.htm', 'NoExtension'
foreach ($name in $names) {
Set-Content -LiteralPath (Join-Path -Path $folder -ChildPath $name) -Value $name -NoNewline
}
$null = New-Item -ItemType Directory -Path (Join-Path -Path $folder -ChildPath 'NoExtensionFolder')
}
# Before 5.0.0, the cmdlet read [1] as a character class and returned nothing.
It 'Should find a file whose name contains brackets by that name with -Filter' {
$result = @(Get-ChildItem2 -Path $folder -Filter 'Report[1].txt' -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$result | Should -HaveCount 1
$result[0].Name | Should -BeExactly 'Report[1].txt'
}
# Before 5.0.0, the cmdlet compared each name with the pattern again and dropped the items without a dot in their names,
# files and folders alike.
It 'Should return every item for -Filter *.*, also the ones without a dot in their names' {
$result = @(Get-ChildItem2 -Path $folder -Filter '*.*' -ErrorVariable operationErrors -ErrorAction SilentlyContinue)
Format-LabError -ErrorRecord $operationErrors | Should -BeNullOrEmpty
$expected = @($names) + 'NoExtensionFolder'
(@($result.Name) | Sort-Object) -join ',' | Should -BeExactly (($expected | Sort-Object) -join ',')
}
# A null value used to end in a NullReferenceException of the cmdlet.
It 'Should reject a null -Filter' {
{ Get-ChildItem2 -Path $folder -Filter $null -ErrorAction Stop } |
Should -Throw -ErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' -ExpectedMessage "*'Filter'*"
}
}
Describe 'Privileges when a later command takes the debug messages of the cmdlet on a share' -Tag 'Delegate', 'Admin' -Skip:(-not $configured) {
# The two roles are administrators of the client, so the cmdlets enable privileges there. Before 5.0.0, a later command that
# ended the pipeline or threw at the message after the enabling left a privilege enabled in the session: the cmdlet had not
# noted yet that it enabled it, so nothing disabled it, and a throw was taken for a failure to enable the privilege.
BeforeAll {
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
$savedEnablePrivileges = $privateData['EnablePrivileges']
$privateData['EnablePrivileges'] = $true
$debugFolder = Get-LabPath -RelativePath "Case10\$Role\Privileges"
$null = New-Item -ItemType Directory -Path $debugFolder -Force
function Get-LabEnabledFileSystemPrivilege {
# The names of the privileges that the cmdlets enable, as far as they are enabled now
@(Get-Privileges | Where-Object -FilterScript {
$_.Privilege -in 'TakeOwnership', 'Restore', 'Backup', 'Security' -and $_.PrivilegeState -eq 'Enabled'
} | ForEach-Object -Process { $_.Privilege.ToString() })
}
}
AfterAll {
$privateData['EnablePrivileges'] = $savedEnablePrivileges
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
BeforeEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
AfterEach {
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
}
It 'Should hold the four privileges that the cmdlets enable' {
@(Get-Privileges | Where-Object -FilterScript { $_.Privilege -in 'TakeOwnership', 'Restore', 'Backup', 'Security' }) | Should -HaveCount 4
}
It 'Should disable the privilege when Select-Object -First ends the pipeline at the message after its enabling' {
$DebugPreference = 'Continue'
$messages = @(Get-NTFSOwner -Path $debugFolder 5>&1 | ForEach-Object -Process { $_.Message })
$enabledAt = $messages.IndexOf('..enabled') + 1
$enabledAt | Should -BeGreaterThan 0
Get-LabEnabledFileSystemPrivilege | Should -BeNullOrEmpty
$result = @(Get-NTFSOwner -Path $debugFolder 5>&1 | Select-Object -First $enabledAt)
$result | Should -HaveCount $enabledAt
$result[-1].Message | Should -BeExactly '..enabled'
Get-LabEnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
It 'Should pass on what a later command throws at the message after the enabling and disable the privileges' {
$DebugPreference = 'Continue'
$caught = $null
try {
Get-NTFSOwner -Path $debugFolder 5>&1 | ForEach-Object -Process {
if ($_.Message -eq '..enabled') { throw 'Downstream failure' }
$_
} | Out-Null
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
Get-LabEnabledFileSystemPrivilege | Should -BeNullOrEmpty
}
}
Describe 'Security descriptors on the file server after the runs on the client' -Tag 'Server' -Skip:(-not $configured) {
It 'Should keep Administrators as the owner of <Folder>' -ForEach $ownedFolders {
Get-LabOwner -Path (Get-LabPath -RelativePath $Folder) | Should -Be $administrators
@ -902,4 +1489,40 @@ Describe 'Security descriptors on the file server after the runs on the client'
@(Get-LabExplicitAccessRule -Path (Get-LabPath -RelativePath 'Case9\ForeignAdd') -Sid $Sid) | Should -HaveCount 1
Get-LabExplicitAccessRule -Path (Get-LabPath -RelativePath 'Case9\ForeignRemove') -Sid $Sid | Should -BeNullOrEmpty
}
# Case 10: a later command stopped each cmdlet after its first item. The first item changed, the second is as it was.
It 'Should have changed only the first item of <StateRole> for each cmdlet that a later command stopped' -ForEach $laterCommandStates {
$root = Get-LabPath -RelativePath "Case10\$StateRole\LaterCommand"
$account = $configuration.Accounts.$StateRole.Sid
foreach ($slug in 'Select', 'Throw') {
$removed = Join-Path -Path $root -ChildPath "RemoveItem2-$slug"
Test-Path -LiteralPath (Join-Path -Path $removed -ChildPath 'First.txt') | Should -BeFalse -Because "Remove-Item2 removed the first item ($slug)"
Test-Path -LiteralPath (Join-Path -Path $removed -ChildPath 'Second.txt') | Should -BeTrue -Because "Remove-Item2 left the second item ($slug)"
$copied = Join-Path -Path $root -ChildPath "CopyItem2-$slug-To"
Test-Path -LiteralPath (Join-Path -Path $copied -ChildPath 'First.txt') | Should -BeTrue -Because "Copy-Item2 copied the first item ($slug)"
Test-Path -LiteralPath (Join-Path -Path $copied -ChildPath 'Second.txt') | Should -BeFalse -Because "Copy-Item2 left the second item ($slug)"
$moved = Join-Path -Path $root -ChildPath "MoveItem2-$slug"
Test-Path -LiteralPath (Join-Path -Path $moved -ChildPath 'First.txt') | Should -BeFalse -Because "Move-Item2 moved the first item ($slug)"
Test-Path -LiteralPath (Join-Path -Path $moved -ChildPath 'Second.txt') | Should -BeTrue -Because "Move-Item2 left the second item ($slug)"
Test-Path -LiteralPath (Join-Path -Path $root -ChildPath "MoveItem2-$slug-To\First.txt") | Should -BeTrue -Because "Move-Item2 moved the first item ($slug)"
Test-Path -LiteralPath (Join-Path -Path $root -ChildPath "MoveItem2-$slug-To\Second.txt") | Should -BeFalse -Because "Move-Item2 left the second item ($slug)"
$owned = Join-Path -Path $root -ChildPath "SetOwner-$slug"
Get-LabOwner -Path (Join-Path -Path $owned -ChildPath 'First.txt') | Should -Be $account -Because "Set-NTFSOwner changed the first item ($slug)"
Get-LabOwner -Path (Join-Path -Path $owned -ChildPath 'Second.txt') | Should -Be $administrators -Because "Set-NTFSOwner left the second item ($slug)"
# The messages come before the change of an item, so the first item may still be as it was.
$ownedAtDebug = Join-Path -Path $root -ChildPath "SetOwner-Debug$slug"
Get-LabOwner -Path (Join-Path -Path $ownedAtDebug -ChildPath 'Second.txt') | Should -Be $administrators -Because "Set-NTFSOwner left the second item at the debug message ($slug)"
$written = Join-Path -Path $root -ChildPath "SetDescriptor-$slug"
@(Get-LabExplicitAccessRule -Path (Join-Path -Path $written -ChildPath 'First.txt') -Sid $everyone) | Should -HaveCount 1 -Because "Set-NTFSSecurityDescriptor wrote the first descriptor ($slug)"
Get-LabExplicitAccessRule -Path (Join-Path -Path $written -ChildPath 'Second.txt') -Sid $everyone | Should -BeNullOrEmpty -Because "Set-NTFSSecurityDescriptor left the second item ($slug)"
$writtenAtVerbose = Join-Path -Path $root -ChildPath "SetDescriptor-Verbose$slug"
Get-LabExplicitAccessRule -Path (Join-Path -Path $writtenAtVerbose -ChildPath 'Second.txt') -Sid $everyone | Should -BeNullOrEmpty -Because "Set-NTFSSecurityDescriptor left the second item at the verbose message ($slug)"
}
}
}

143
Tests/Lab/Non-Windows-File-Server-Test.md

@ -0,0 +1,143 @@
# Test NTFSSecurity on a file server that isn't Windows
This page is for people who reported [#34][issue-34] on a NetApp, EMC, IBM, or
other file server and who offered to test a fix. It takes about 20 minutes.
Two people may share the work: a storage administrator, who prepares and
removes a test folder, and a user without administrator rights on the file
server, who runs the module. The commands are the ones that the
[live tests](README.md) run on Windows file servers (case 1).
## Keep it safe
- Use only a new folder that you create for this test. Never run these
commands on real data, on the root of a share, or on a home folder: they
change permissions.
- The test removes nothing outside that folder. When you finish, the
storage administrator deletes the folder.
- Don't send passwords, API keys, file contents, or complete security
descriptors. Replace the names of servers, domains, and accounts with
placeholders, such as `FILER`, `DOMAIN`, and `testuser`. Well-known SIDs,
such as `S-1-5-32-544`, can stay.
## What you need
- The exact package to test. The maintainer names it in the issue; this page
says `5.0.0-rc7` as an example. Install it in a new PowerShell session and
don't load another version of NTFSSecurity in the same session:
```powershell
Install-Module -Name NTFSSecurity -RequiredVersion 5.0.0-rc7 -AllowPrerelease -Scope CurrentUser
Import-Module -Name NTFSSecurity
(Get-Module -Name NTFSSecurity).Version
(Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path -Path (Get-Module -Name NTFSSecurity).ModuleBase -ChildPath 'NTFSSecurity.dll')).Hash
```
- A domain group that has Full Control on the test folder, and a user who is
a member of that group but not an administrator (or root) of the file
server. This is the setup in which the error 1307 happened.
- Windows PowerShell 5.1 or PowerShell 7. Say which one you used.
## 1. Prepare the test folder (storage administrator)
Create the folder and one subfolder for each command. The user who runs the
module in step 2 must not own these folders: in the failing setup the folder
is owned by `BUILTIN\Administrators`, or by the owner that your file server
shows for administrators, and the user may not assign that owner. Replace the
first two lines.
```powershell
$root = '\\FILER\share\ntfssecurity-test'
$group = 'DOMAIN\ntfssecurity-test-group'
$names = 'AddAccess', 'RemoveAccess', 'ClearAccess', 'DisableInheritance', 'EnableInheritance', 'SetInheritance', 'SetSecurityDescriptor'
New-Item -ItemType Directory -Path $root | Out-Null
icacls $root /grant "${group}:(OI)(CI)F" | Out-Null
foreach ($name in $names) { New-Item -ItemType Directory -Path (Join-Path $root $name) | Out-Null }
icacls "$root\RemoveAccess" /grant 'Everyone:(OI)(CI)RX' | Out-Null
icacls "$root\ClearAccess" /grant 'Everyone:(OI)(CI)RX' | Out-Null
icacls "$root\EnableInheritance" /inheritance:d | Out-Null
(Get-Acl -LiteralPath $root).Owner
```
The last line shows the owner. If it is the account that runs the module in
step 2, the test can't show the error: let another administrator create the
folders. If `icacls` fails for you at this step, stop and tell us.
## 2. Run the commands (the user without administrator rights)
Run this in the new session in which you imported NTFSSecurity. It only
changes the seven subfolders. Each command writes an error, if there is one,
instead of stopping.
```powershell
$root = '\\FILER\share\ntfssecurity-test'
$everyone = 'Everyone'
$ErrorActionPreference = 'Continue'
function Show-State ([string] $Name) {
$path = Join-Path $root $Name
'--- {0}: owner {1}' -f $Name, ((Get-Acl -LiteralPath $path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value)
icacls $path
}
foreach ($name in 'AddAccess', 'RemoveAccess', 'ClearAccess', 'DisableInheritance', 'EnableInheritance', 'SetInheritance', 'SetSecurityDescriptor') { Show-State $name } # before
Add-NTFSAccess -Path "$root\AddAccess" -Account $everyone -AccessRights ReadData
Remove-NTFSAccess -Path "$root\RemoveAccess" -Account $everyone -AccessRights ReadAndExecute -InheritanceFlags 'ContainerInherit, ObjectInherit' -PropagationFlags None
Clear-NTFSAccess -Path "$root\ClearAccess"
Disable-NTFSAccessInheritance -Path "$root\DisableInheritance"
Enable-NTFSAccessInheritance -Path "$root\EnableInheritance"
Set-NTFSInheritance -Path "$root\SetInheritance" -AccessInheritanceEnabled $false
$descriptor = Get-NTFSSecurityDescriptor -Path "$root\SetSecurityDescriptor"
Add-NTFSAccess -SecurityDescriptor $descriptor -Account $everyone -AccessRights ReadData
Set-NTFSSecurityDescriptor -SecurityDescriptor $descriptor
foreach ($name in 'AddAccess', 'RemoveAccess', 'ClearAccess', 'DisableInheritance', 'EnableInheritance', 'SetInheritance', 'SetSecurityDescriptor') { Show-State $name } # after
```
What a good result looks like, for each of the seven folders:
- The command wrote no error.
- The owner is the same before and after.
- Only the intended entry changed: `AddAccess` gained an entry for Everyone,
`RemoveAccess` and `ClearAccess` lost theirs, the inheritance flags of
`DisableInheritance`, `EnableInheritance`, and `SetInheritance` changed,
and `SetSecurityDescriptor` gained an entry for Everyone.
## 3. Tell us what happened
Post a comment in [#34][issue-34] with:
1. The package version and the SHA-256 of `NTFSSecurity.dll` from the
first step, and the PowerShell edition and Windows version of the
computer that ran the commands.
2. The file server product and version, such as `ONTAP 9.x`, `PowerScale
OneFS x.y`, or `IBM ESS x.y`, whether the path goes through DFS, and the
SMB version if you know it.
3. For each of the seven commands: worked or failed. For a failure, the
first line of the error and its `FullyQualifiedErrorId`, such as
`AddAceError,NTFSSecurity.AddAccess`.
4. The owner before and after for each folder, and the `icacls` output
before and after, with the names replaced.
5. Anything that looked different from what you expected, even if the
commands worked.
A result that says only "works for me" can't tell us which command ran on
which setup. The list above is what we need to treat the result as a test.
## 4. Optional: confirm that your setup reproduces the error
To see that the setup is the one in which #34 happened, repeat the second
step with `4.2.6` in a new session. Reporters saw error 1307 from
`Add-NTFSAccess` in 4.2.6, and in our Windows lab 5.0.0-rc2 failed
`Add-NTFSAccess`, `Clear-NTFSAccess`, and `Set-NTFSSecurityDescriptor` the
same way. Create a new test folder first, because the first run changed some
of the seven folders. Don't run the two versions in the same session.
## 5. Remove the test folder (storage administrator)
Delete the folder `ntfssecurity-test` with its subfolders. Check its path
first, so that you delete only the test folder.
[issue-34]: https://github.com/raandree/NTFSSecurity/issues/34

119
Tests/Lab/README.md

@ -13,7 +13,7 @@ without a lab they skip every test.
| --- | --- | --- |
| 1, [#34][issue-34] | Delegate | `Add-NTFSAccess`, `Remove-NTFSAccess`, `Clear-NTFSAccess`, `Disable-NTFSAccessInheritance`, `Enable-NTFSAccessInheritance`, `Set-NTFSInheritance`, and `Set-NTFSSecurityDescriptor` on share folders that Administrators own and on which a domain group has Full Control, run by a member of that group who isn't an administrator of the file server. They succeed and keep the owner. |
| 2 | Admin, ServerAdmin, Delegate | `Get-NTFSAudit`, `Add-NTFSAudit`, and `Remove-NTFSAudit` on share folders. Over SMB, the file server checks the Security privilege of the account. The administrators of the file server read and change the audit entries; the delegated account gets the errors that the cmdlet pages describe, and the folders stay unchanged. |
| 3 | Admin, Delegate | `Get-NTFSEffectiveAccess` for a domain account with rights through two nested domain groups and through a local group of the file server. With `-ServerName`, the result includes the local group, without a warning; without it, the client doesn't know that group. With an unreachable server, the cmdlet falls back to the client and warns. The authorization manager of the file server refuses the delegated account, which isn't an administrator there, and the cmdlet reports that as an error, not as no access. |
| 3 | Admin, ServerAdmin, Delegate | `Get-NTFSEffectiveAccess` for a domain account with rights through two nested domain groups and through a local group of the file server. With `-ServerName`, the result includes the local group, without a warning; without it, the client doesn't know that group. With an unreachable server, the cmdlet falls back to the client and warns. The authorization manager of the file server refuses the delegated account, which isn't an administrator there, and the cmdlet reports that as an error, not as no access. The administrator of the file server who isn't an administrator of the client gets the result of the client without `-ServerName` and for the name of the client, because the local authorization manager of the client answers when the remote one refuses a user who isn't its administrator. |
| 4 | Admin | `Get-NTFSOrphanedAccess` returns the entry of a deleted domain account with its SID, on the folder and as inherited entry on a file in it; `Get-NTFSOrphanedAudit` returns the audit entry of that account. |
| 5 | Admin, ServerAdmin, Delegate | `Get-NTFSOwner` and `Set-NTFSOwner` on share folders that Administrators own. Every role makes itself the owner; only the administrators of the file server, which hold the Restore privilege there, assign another account. The delegated account gets a `SetOwnerError`, and the owner stays. |
| 6 | Admin, ServerAdmin, Delegate | `Disable-NTFSAuditInheritance`, `Enable-NTFSAuditInheritance`, `Clear-NTFSAudit`, and `Get-NTFSInheritance` on share folders that inherit an audit entry. The administrators of the file server change the audit entries; the delegated account gets the errors that the cmdlet pages describe, and `Get-NTFSInheritance` reports no audit state for it. |
@ -22,7 +22,8 @@ without a lab they skip every test.
| 9 | Delegate, Admin | `Get-NTFSSimpleAccess` compares a share folder with its parent. For the accounts of another domain and of other forests, `Get-NTFSAccess` returns their names, `Get-NTFSOrphanedAccess` doesn't report them, `Add-NTFSAccess` and `Remove-NTFSAccess` find them by name, and `Get-NTFSEffectiveAccess -ServerName` returns the rights that the file server's own token of each account gets. |
| Long paths | Admin | `Get-ChildItem2` and `Get-NTFSAccess` with a share path longer than 260 characters. |
| [#108][issue-108] | Admin | `Copy-Item2` and `Move-Item2` with `-WhatIf` onto an existing file on the share write no error. |
| State | Server | After the runs on the client, the file server checks the owners, the audit entries, the items, the links, and the entries of the foreign accounts itself, without the module. |
| 10 | Delegate, ServerAdmin, Admin | The behavior that the quality-gate fixes before 5.0.0 changed, and that the lab can observe. The delegated account, which owns the items it creates, clears and protects the DACL of an item whose OWNER RIGHTS entry denies it the right to change the DACL, and the cmdlets report no `RestoreOwnerError` for the unchanged owner. `InheritedFrom` names an `unknown parent` for entries that Windows can't resolve, for a deleted file and below a folder whose permissions the account can't read, and no source for an explicit entry. A later command that stops the pipeline with `Select-Object -First 1` or throws leaves the second item of `Remove-Item2`, `Copy-Item2`, `Move-Item2`, `Set-NTFSOwner`, and `Set-NTFSSecurityDescriptor` as it was, also when it takes the verbose messages of `Set-NTFSSecurityDescriptor` or the debug messages of `Set-NTFSOwner`, and `Get-FileHash2` writes no error when it takes its verbose messages. `Get-ChildItem2` passes on what a later command throws for the error of a folder it can't read, and a `break` of that command leaves the caller's loop. `Get-ChildItem2 -Filter` finds a name with brackets, returns every item for `*.*`, and rejects `$null`. The privileges that the cmdlets enable are disabled again when a later command stops the pipeline or throws at a debug message. |
| State | Server | After the runs on the client, the file server checks the owners, the audit entries, the items, the links, the entries of the foreign accounts, and which items a later command changed, itself, without the module. |
Case 1 uses two kinds of folders. Before 5.0.0-rc3, the cmdlets wrote back the
owner that Windows returns with a DACL without the auto-inherit flag, and the
@ -46,11 +47,24 @@ of the foreign accounts the same way, on the file server.
| Admin | `NtfsLiveAdmin` | Yes | Yes |
| Server | The installation account of the lab, on the file server | Yes | Yes |
The script also creates `NtfsLiveSubject`, the account of case 3, which is a
member of `NtfsLiveInner`, a member of `NtfsLiveOuter`, and of the local group
`NtfsLiveLocal` of the file server, and `NtfsLiveOrphan`, which it deletes in
every run. For case 9, it creates `NtfsLiveForeign` in the organizational unit
`NTFSSecurityLive` of each domain of `-ForeignDomainController`.
The script also creates the account of case 3, `NtfsLiveSubject` followed by
four digits, which is a member of `NtfsLiveInner`, a member of `NtfsLiveOuter`,
and of the local group `NtfsLiveLocal` of the file server, and `NtfsLiveOrphan`,
which it deletes in every run. For case 9, it creates `NtfsLiveForeign` in the
organizational unit `NTFSSecurityLive` of each domain of
`-ForeignDomainController`.
A new fixture gets a new name for the account of case 3. In the matrix lab, the
authorization managers that `Get-NTFSEffectiveAccess` asks for a remote computer
(the one of the client by the default `-ServerName`, the one of the file server
by its name) answered for about ten minutes as if an account had no groups when
the account was deleted and created again under the same name, so cells that
followed each other failed in the effective-access tests, for the baseline and
for the final candidate alike. The Kerberos logon that the tests use as the
oracle, and the
local authorization manager, were right in the same second. The mechanism in
Windows isn't known (see the record of the operating-system matrix). A fixture
that exists keeps its account, so the runs of one fixture use one name.
## Lab
@ -67,6 +81,20 @@ domain of the same forest, and `F2DC1` and `F3DC1` of the forests
`forest2.net` and `forest3.net`, which have forest trusts with `forest1.net`;
`-ForeignDomainController @()` leaves it out.
The operating-system matrix has a lab of its own, `NtfsSecurityOsMatrixLab`,
on the same host. `Acceptance\Deploy-OsMatrixLab.ps1` deploys it in AutomatedLab
without touching another lab: `OSDC1` (Windows Server 2025), the domain
controller of the domain `osmatrix.net`, the file servers `OSFile19`,
`OSFile22`, and `OSFile25` (Windows Server 2019, 2022, and 2025), the client
`OSWin11E` (Windows 11 Enterprise Evaluation 22H2), and `OSWin11` (Windows 11 Pro
26H1), on a switch of their own, `192.168.12.0/24`. `OSWin11` can't keep a
secure channel to the domain controller, so it runs the suite of the module
only (`Run-MatrixLocalSuite.ps1`, below). The evaluation image of `OSWin11E`
shuts down an hour after each start, because its license period has ended: start
it shortly before a run and keep a run under an hour. The lab has no trust with
another forest, so case 9 runs only in the first lab and the matrix runs use
`-ForeignDomainController @()`.
The script adds to the lab:
- the organizational unit `NTFSSecurityLive` with the accounts and groups, and
@ -122,7 +150,10 @@ A version before 5.0.0-rc3 fails case 1 with error 1307, a version before
test of case 3 with a computer that can't be reached: it returned no access
instead of the result of the client. A version before 5.0.0-rc6 fails two
tests of case 8: `Get-NTFSHardLink` and `New-NTFSHardLink -PassThru` stopped
on the share with the terminating error (50).
on the share with the terminating error (50). A build without the fixes of the
quality gate before 5.0.0 fails case 10 and the matching test of the State
role: 74 of the 244 tests of each edition (see the
[record of that run](Acceptance-2026-10-09-quality-gate-paths.md)).
## Acceptance of a release candidate
@ -141,11 +172,74 @@ and record the evidence in this folder:
5. Remove the fixture with `-RemoveFixture` and check that its accounts,
share, folders, group memberships, and profiles are gone.
A run with `-ModulePath` validates a build. The acceptance of a release is the
run with `-Version` of the exact prerelease on the PowerShell Gallery, as
`Docs\Contributing\05-Releasing.md` describes. The scripts in the folder
[Acceptance](Acceptance) support it, and each runs in Windows PowerShell 5.1 on
the host:
- `Test-PublishedRelease.ps1 -Version <version> -OutputPath <folder>` checks a
published version and changes nothing: the tag and its commit on `master`,
the CI run of the tag, the SHA-512 that the PowerShell Gallery publishes
against the downloaded package, and the module files of the package against
those of the GitHub zip file.
- `Validate-LabResults.ps1` checks every role of a controller run from the
result files, not from the marker `DONE` that the controller writes also when
tests failed.
- `Run-MatrixSequence.ps1` runs steps 2, 4, and 5 for each file server of the
matrix lab with the client `OSWin11E`: it checks the readiness of every
machine (`Test-MatrixReadiness.ps1`), runs the controller in both editions,
validates every role, removes the fixture, and checks the end state
independently (`Test-MatrixCleanup.ps1`, which takes the lab name and the
machines, so it checks the first lab as well; `-Mode Repair` removes what a
failed cleanup left, and what the probes and the suite runner of the kit leave,
which includes every unresolved `S-1-5-21-…` member of Performance Log Users:
the check treats it as the probe's).
- `Run-MatrixLocalSuite.ps1` runs the Pester files of the module on a machine of
the matrix lab, or on the host as the reference, in both editions, elevated
and as a basic user, and copies the results back. Run it with a candidate
before the controller: a test that passes on a computer outside a domain can
fail on a computer in a domain, as the effective-access tests of the basic
user did. `Export-MatrixResults.ps1` turns the results into tables.
- `Probe-EffectiveAccess.ps1` asks `Get-NTFSEffectiveAccess` the same questions
under the token of an administrator, a filtered administrator, a local
standard user, and a standard domain user, and shows which authorization
manager answers or refuses.
- `Probe-AccountRecreation.ps1` deletes an account and creates it again with the
same name in a loop. It shows the SID and the groups that Kerberos S4U logons
report on the domain controller, the client, and the file server, and what
`Get-NTFSEffectiveAccess` of each module under test returns. It showed the
state that the controller avoids with a new name for the account of case 3.
- `Export-CellTimeline.ps1` reads the sequence and run logs of controller cells
and writes one row for every cell and edition in which the Admin role ran: the
module, the account and its relative ID, whether the previous cell had the same
name and the same account, the times of the removal of the previous fixture, of
the creation of the accounts, and of the Admin role, and the three
effective-access tests of case 3.
- `Test-StaleAuthzModel.ps1` replays such a timeline against the model of the
failures of the effective-access tests (an authorization manager that answers
for an account name from its first request, for some minutes, also after the
account was created again) and reports, for the lifetimes that predict most
outcomes, how many of the observed ones the model reproduces. With `-Lifetime`
it lists every run with the observed and the predicted outcome for that one
lifetime, with `-AsIfSameSubject` it shows where a controller that reuses
the account name would have met a stale entry, and with `-Permutations` how
often a random assignment of the outcomes fits as well. It describes the
observations; it doesn't explain Windows.
- `Add-OsMatrixMachine.ps1`, `Complete-OsMatrixLab.ps1`, and
`Repair-OsMatrixBoot.ps1` add a machine to the deployed lab, install the tools
on the machines (the VMs have no internet), and repair the boot files of a
base image that the host couldn't write.
- `Probe-LaterCommand.ps1` is the diagnostic of the
[record of the paths fixes](Acceptance-2026-10-09-quality-gate-paths.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), and
[quality-gate follow-up](Acceptance-2026-10-09-quality-gate.md). The review
of the code that no unit test visits, with the fixes that the lab has to
repeat, is in
[5.0.0-rc7](Acceptance-2026-10-08-5.0.0-rc7.md),
[quality-gate follow-up](Acceptance-2026-10-09-quality-gate.md),
[quality-gate paths follow-up](Acceptance-2026-10-09-quality-gate-paths.md), and
[operating-system matrix](Acceptance-2026-10-10-os-matrix.md).
The review of the code that no unit test visits, with the fixes that the lab
has to repeat, is in
[Tests/Coverage](../Coverage/Quality-Gate-Paths-2026-10-09.md).
## Files
@ -156,6 +250,7 @@ repeat, is in
| `NTFSSecurity.Live.Tests.ps1` | The tests; run on the client and the file server. |
| `Start-NTFSSecurityLiveTest.ps1` | Runs the tests of one role in a new process. |
| `NTFSSecurity.LabHelpers.ps1` | Reads and writes security descriptors as Windows stores them, and calculates the expected rights. |
| `Acceptance\` | The scripts of the acceptance of a candidate or of a published version, and the diagnostic of the paths record (see above). |
[issue-34]: https://github.com/raandree/NTFSSecurity/issues/34
[issue-108]: https://github.com/raandree/NTFSSecurity/issues/108

Loading…
Cancel
Save