mirror of https://github.com/raandree/NTFSSecurity
committed by
GitHub
25 changed files with 1710 additions and 602 deletions
@ -0,0 +1,105 @@ |
|||||
|
<# |
||||
|
.SYNOPSIS |
||||
|
Publishes the already built NTFSSecurity package to the PowerShell Gallery. |
||||
|
.DESCRIPTION |
||||
|
Uses the PSGALLERY_API_KEY environment secret. A published version is skipped only when its Gallery SHA512 |
||||
|
matches the exact local package. An uncertain upload is recovered only after that same verification; other |
||||
|
errors remain failures. The release workflow checks the tag and version first. |
||||
|
.PARAMETER NupkgPath |
||||
|
The package that the build job produced. |
||||
|
.PARAMETER Version |
||||
|
The version that the release workflow verified. |
||||
|
.EXAMPLE |
||||
|
.\.github\scripts\Publish-ModulePackage.ps1 -NupkgPath .\out\NTFSSecurity.5.0.0-rc7.nupkg -Version 5.0.0-rc7 |
||||
|
|
||||
|
Publishes the package using the environment secret, without logging or passing the key on a process command line. |
||||
|
#> |
||||
|
[CmdletBinding()] |
||||
|
param ( |
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] |
||||
|
[string] $NupkgPath, |
||||
|
|
||||
|
[Parameter(Mandatory)] |
||||
|
[ValidatePattern('\A\d+\.\d+\.\d+(?:-[A-Za-z][0-9A-Za-z-]*)?\z')] |
||||
|
[string] $Version |
||||
|
) |
||||
|
|
||||
|
$ErrorActionPreference = 'Stop' |
||||
|
if (-not $env:PSGALLERY_API_KEY) { |
||||
|
throw 'The secret PSGALLERY_API_KEY of the environment powershell-gallery is not set.' |
||||
|
} |
||||
|
$packagePath = (Resolve-Path -LiteralPath $NupkgPath).ProviderPath |
||||
|
|
||||
|
function Find-PublishedPackage { |
||||
|
[CmdletBinding()] |
||||
|
[OutputType([psobject])] |
||||
|
param () |
||||
|
|
||||
|
$lookupErrors = @() |
||||
|
$found = @(Find-PSResource -Name NTFSSecurity -Version $Version -Prerelease -Repository PSGallery -ErrorAction SilentlyContinue -ErrorVariable lookupErrors) |
||||
|
foreach ($lookupError in $lookupErrors) { |
||||
|
if (($lookupError.FullyQualifiedErrorId -split ',')[0] -ne 'PackageNotFound') { |
||||
|
throw $lookupError |
||||
|
} |
||||
|
} |
||||
|
if ($found.Count -gt 1) { |
||||
|
throw "The PowerShell Gallery returned more than one package for NTFSSecurity $Version." |
||||
|
} |
||||
|
if ($found.Count -eq 1) { |
||||
|
return $found[0] |
||||
|
} |
||||
|
Write-Verbose "NTFSSecurity $Version is not listed in the PowerShell Gallery." |
||||
|
} |
||||
|
|
||||
|
function Assert-PublishedPackage { |
||||
|
[CmdletBinding()] |
||||
|
param () |
||||
|
|
||||
|
$uri = "https://www.powershellgallery.com/api/v2/Packages(Id='NTFSSecurity',Version='$Version')" |
||||
|
$entry = Invoke-RestMethod -Uri $uri -ErrorAction Stop |
||||
|
$expectedHash = [string] $entry.entry.properties.PackageHash |
||||
|
if ($entry.entry.properties.PackageHashAlgorithm -ne 'SHA512' -or -not $expectedHash) { |
||||
|
throw "The PowerShell Gallery has no usable SHA512 hash for NTFSSecurity $Version." |
||||
|
} |
||||
|
$stream = [IO.File]::OpenRead($packagePath) |
||||
|
$sha512 = [Security.Cryptography.SHA512]::Create() |
||||
|
try { |
||||
|
$actualHash = [Convert]::ToBase64String($sha512.ComputeHash($stream)) |
||||
|
} |
||||
|
finally { |
||||
|
$sha512.Dispose() |
||||
|
$stream.Dispose() |
||||
|
} |
||||
|
if ($actualHash -cne $expectedHash) { |
||||
|
throw "NTFSSecurity $Version in the PowerShell Gallery contains a different package; publication cannot continue." |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Find-PublishedPackage) { |
||||
|
Assert-PublishedPackage |
||||
|
"NTFSSecurity $Version is already in the PowerShell Gallery and matches the exact local package." |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
Publish-PSResource -NupkgPath $packagePath -Repository PSGallery -ApiKey $env:PSGALLERY_API_KEY -ErrorAction Stop |
||||
|
} |
||||
|
catch { |
||||
|
$publishError = $_ |
||||
|
$verified = $false |
||||
|
try { |
||||
|
if (Find-PublishedPackage) { |
||||
|
Assert-PublishedPackage |
||||
|
$verified = $true |
||||
|
} |
||||
|
} |
||||
|
catch { |
||||
|
Write-Warning ("The upload outcome could not be verified for NTFSSecurity {0}: {1}" -f $Version, $_.Exception.Message) |
||||
|
} |
||||
|
if ($verified) { |
||||
|
Write-Warning "Publish-PSResource reported an error, but the Gallery SHA512 verified the exact package for NTFSSecurity $Version." |
||||
|
return |
||||
|
} |
||||
|
throw $publishError |
||||
|
} |
||||
@ -1,203 +1,121 @@ |
|||||
--- |
--- |
||||
status: current |
status: current |
||||
last-verified: 2026-10-08 |
last-verified: 2026-10-09 |
||||
owner: active-agent |
owner: active-agent |
||||
source: repository evidence |
source: repository and validation evidence |
||||
--- |
--- |
||||
|
|
||||
# Progress |
# Progress |
||||
|
|
||||
## Current status |
## Current status |
||||
|
|
||||
5.0.0-rc6 is on the PowerShell Gallery, published by CI on 2026-10-08 at |
5.0.0-rc6 is published on the Gallery and GitHub; its failed Release job |
||||
20:40 UTC from the tag `5.0.0-rc6` on `master` (`b51d970`, the merge of |
recovered in attempt 2 on 2026-10-09. #116 (rc7, `d25647d`, base `master`) |
||||
pull request #115; Decision 12). The Release job failed after the upload, |
is open and green, not merged or published. Further quality-gate work is |
||||
so the GitHub release waits for a rerun of the failed job. The published |
local on `ai/quality-gate-coverage`; Phase 2 is not complete while the |
||||
package passed the live tests. Phase 2 of the quality gate (Decision 21) |
remaining-path inventory is open. Stable Gallery version: 4.2.6. |
||||
is complete. The pull request #116 (`ai/release-5.0.0-rc7`) holds the |
After 5.0.0, archive in favor of WindowsAccessControl (Decision 18). |
||||
behavior changes that Phase 2 found, decided as assumptions for the |
|
||||
maintainer's review (Decision 22), and waits for that review. Phase 3 |
|
||||
follows. The stable Gallery version is still 4.2.6. NTFSSecurity will be |
|
||||
archived soon; its users move to WindowsAccessControl (Decision 18). |
|
||||
|
|
||||
## Recent milestones |
## Recent milestones |
||||
|
|
||||
- 2026-10-02 to 2026-10-04: #91 to #97 aligned the docs with the code, |
- 2026-10-02 to 2026-10-06: documentation/help aligned with source, CI and |
||||
shipped the help file, kept the docs on GitHub, set version 5.0.0, moved |
wiki moved to GitHub Actions, versioning/release automation established, |
||||
CI and a wiki generated from `Docs` to GitHub Actions, and completed the |
and prereleases rc1 to rc4 published. Earlier detail is in git, |
||||
version history (Decisions 6 to 11). |
`CHANGELOG.md`, `Docs/Version-History.md`, and Decisions 1 to 19. |
||||
- 2026-10-04: #98 (`e0f5366`) added releases on a version tag through CI |
- 2026-10-07: lab comparison of rc2/rc4 reproduced #34 over SMB and proved |
||||
(Decision 12). The tag `5.0.0-rc1` published to the Gallery and created |
changed-section writes preserve the owner. Remote effective-access |
||||
the GitHub prerelease; the installed module passed the full suite. |
fallback returned no result; fixed test-first for rc5 (Decision 20). |
||||
- 2026-10-05, overnight run: the 24 code defects of work package 5 and the |
- 2026-10-08: #114 merged (`fcb370e`), rc5 published and live-tested. |
||||
issues #3, #4, #5, #17, #74, #82, #86, and #88 fixed with regression |
Decision 21 established the quality gate. Corrected four-run coverage: |
||||
tests, plus what one security review per PR found; the 37 open issues |
rc5 58.1% sequence points/38.0% branches, not the initial one-run result. |
||||
triaged; `Docs/FAQ.md`, Dependabot for the actions, and the label `rc2`. |
- 2026-10-08: rc6 Phase 2 added basic-user CI, parameter-set/error tests, |
||||
#5 and #82 are breaking changes, like the change of Decision 13. |
expanded domain/SMB cases, and fixes for privilege cleanup, path |
||||
- 2026-10-05: the first CI runs of the eight PRs found a defect that the |
resolution, ownership retries, item conflicts, output equality, and |
||||
workstation had skipped, fixed in `629f4e7` (audit inheritance of an item |
inherited flags. Suite: 677; coverage 68.14% sequence/44.32% branches. |
||||
without a SACL). The PRs #99 to #106 were merged in order with merge |
Lab acceptance of `7b0781f` passed; two independent review passes. |
||||
commits, CI on `master` passed, and the tag `5.0.0-rc2` published the |
- 2026-10-08: rc7 implemented proposed Decision 22, including breaking |
||||
prerelease; GitHub's Actions outage that day cancelled the first two |
link binding/error changes, SimpleAccess traversal, cross-volume folder |
||||
attempts of the release run before they started. Repository hardening is |
preservation, and named effective-access warnings. Suite: 712, no |
||||
optional (Decision 14); the merge rule and the maintainer's rule for |
failures or test skipped everywhere. One review: no Blocker/Major. |
||||
fixes are Decisions 15 and 16. |
Lab candidate `dc6e9f5`: 326 passed, 2 skipped, cleanup verified. |
||||
- 2026-10-06: the issues got their labels (Decision 17). The maintainer |
- 2026-10-08: #115 merged (`b51d970`) and tagged rc6. Release run |
||||
decided to publish 5.0.0-rc3 before 5.0.0 and to archive the project in |
`37839669028` failed with HTTP 409 after Gallery publication. The log |
||||
favor of WindowsAccessControl (Decision 18); the README, the docs home, |
does not establish the previously assumed initial timeout/retry cause. |
||||
and the changelog announce it. |
Published rc6 live tests differed only in rc7's warning expectation. |
||||
- 2026-10-06: 5.0.0-rc3 (#112): the access and audit cmdlets write only |
- 2026-10-09: release attempt 2 succeeded; rc6 GitHub prerelease and zip |
||||
the section that they change, which fixes #34 and the inherited entries |
appeared at 07:01:34 UTC. #116 passed CI on `d25647d`. |
||||
that elevated sessions copied as explicit ones (Decision 19). #67 has its |
- 2026-10-09: autonomous follow-up on `ai/quality-gate-coverage`, through |
||||
cause outside the module (a share root over UNC can't re-inherit), is |
`3442194`, adds 202 cases above rc7: deletion/owner failures, all 13 |
||||
explained in `Docs/FAQ.md`, and was closed as not planned. One |
scopes, inheritance transitions, enumeration, forced replacement, |
||||
`security-reviewer` pass approved the branch. The PR description said |
descriptor failures, and offline CI recovery. Reproduced/fixed rooted |
||||
"fixes #34", so the merge closed #34; it was reopened for a tester. |
result-path handling and first-hidden-item omission. Publication recovery |
||||
- 2026-10-06: 5.0.0-rc4 (#113), test-first: drive and volume roots read |
verifies exact SHA-512 identity, not merely version existence. |
||||
and change their root folder, not the device (#41); the audit cmdlets |
- 2026-10-09: final uninstrumented suite: 914 per configuration, zero |
||||
reject a descriptor without the audit entries (#109); `-WhatIf` previews |
failures; coverage: 2,641/3,559 sequence points (74.21%) and 974/1,933 |
||||
`Copy-Item2` and `Move-Item2` despite an existing destination (#108); |
branches (50.39%), aggregate of four runs without AltCover `--save`. |
||||
small items (#111). One `security-reviewer` pass approved it; its Minor |
All skipped templates have executed counterparts. Mutation guards were |
||||
findings R1, R2, R6, and R8 were fixed before the merge. #41, #108, |
proved and production source restored; Release build/checks pass. |
||||
#109, and #111 closed as completed, #90 and #107 as not planned. |
- 2026-10-09: live comparison, 09:20 to 09:51 UTC: candidate 330 passed, |
||||
- 2026-10-07: live tests in the lab of WindowsAccessControl (Decision 20) |
zero failed, two expected skips; published rc6 four expected Hidden/ |
||||
against 5.0.0-rc2 and 5.0.0-rc4 in both editions: rc2 fails #34 over SMB |
warning-text failures only. Independent cleanup probes verified fixture |
||||
with error 1307 for `Add-NTFSAccess`, `Clear-NTFSAccess`, and |
absence; raw host-verifier failures retained with corrected verification. |
||||
`Set-NTFSSecurityDescriptor`; rc4 passes the four cases, except |
Lab guards/acceptance committed in `7594e0c`. One independent code review |
||||
`Get-NTFSEffectiveAccess -ServerName` with a computer that can't be |
approved with no significant finding (custom model unavailable; built-in |
||||
reached, which returned no access since before rc1. The maintainer chose |
fallback). All 11 tested files match the ZIP. OS/path gates stay open. |
||||
to fix it test-first in 5.0.0-rc5; the branch build passes all live tests |
|
||||
and the suite. One `security-reviewer` pass approved it with minor |
|
||||
findings (`activeContext.md`). |
|
||||
- 2026-10-08: #114 merged (`fcb370e`); the tag `5.0.0-rc5` published it to |
|
||||
the Gallery and the GitHub releases, whose `NTFSSecurity.zip` holds the |
|
||||
same 11 files. Phase 1 of the quality gate (Decision 21) measured rc5: |
|
||||
the published package passes the live tests in both editions; the 11 |
|
||||
tests that need a session without the Security privilege pass as a basic |
|
||||
user, so every test runs in at least one configuration, but CI runs only |
|
||||
elevated; the suite runs 55.9% of the C# lines and 37.4% of the branches. |
|
||||
The maintainer approved Phase 2. |
|
||||
- 2026-10-08: Phase 2, step 1 on `ai/release-5.0.0-rc6` (local): tests for |
|
||||
`Set-NTFSOwner`, `Test-Path2`, `Get-DiskSpace`, and the link cmdlets |
|
||||
(suite: 555 tests). Fixed test-first: the privileges stayed enabled after |
|
||||
an early stop; `Test-Path2` stopped for invalid characters in Windows |
|
||||
PowerShell; and, from one `security-reviewer` pass, the privilege cleanup |
|
||||
decided on stale states, a defect since 4.2.6. The page of |
|
||||
`New-NTFSSymbolicLink` was corrected after a lab check of Developer Mode. |
|
||||
- 2026-10-08: Phase 2 finished on `ai/release-5.0.0-rc6` (local). Tests for |
|
||||
`Get-NTFSOrphanedAudit`, `Get-NTFSSimpleAccess`, the |
|
||||
`-SecurityDescriptor` parameter sets, the error contracts of all path |
|
||||
cmdlets, and #110. Fixed test-first: `Get-NTFSSimpleAccess` (`ReadData`, |
|
||||
relative paths), `Copy-Item2` and `Move-Item2` (folder conflicts, the |
|
||||
missing destination folder of #21), the hard-link cmdlets on shares, |
|
||||
`Set-NTFSSecurityDescriptor -PassThru` (R5), and the error ID of |
|
||||
`Get-NTFSOrphanedAccess`; from the coverage report, relative paths that |
|
||||
start with a dot (every cmdlet acted on the item without the first two |
|
||||
characters), comparing output objects (`InvalidCastException`), and |
|
||||
`InheritedFrom`. CI runs the suite as a basic user too; the live tests |
|
||||
cover all cmdlet groups and accounts of three more domains. Suite: 677 |
|
||||
tests, none failed, none skipped in every configuration; C# coverage |
|
||||
68.1% of the lines and 44.3% of the branches (rc5: 58.1% and 38.0%, |
|
||||
measured again; the first measurements counted one of four runs). Two |
|
||||
`security-reviewer` passes; the lab acceptance of `7b0781f` passed |
|
||||
(`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`). |
|
||||
- 2026-10-08: #115 (rc6, head `be04cb7`) passed CI in all four |
|
||||
configurations. On `ai/release-5.0.0-rc7` (local), the behavior changes |
|
||||
of Phase 2 were decided as assumptions for review (Decision 22) and |
|
||||
implemented test-first, two of them breaking (the link cmdlets); new |
|
||||
defects found on the way: `Move-Item2` deleted an empty folder that it |
|
||||
moved to another volume, the link cmdlets failed for every piped object, |
|
||||
and `Get-NTFSEffectiveAccess` warned for names of this computer. One |
|
||||
`security-reviewer` pass (no Blocker or Major; its findings fixed but |
|
||||
one, declined). Suite and lab acceptance in `activeContext.md`. |
|
||||
- 2026-10-08: #115 merged (`b51d970`) and tagged `5.0.0-rc6`. The Release |
|
||||
job published the package at 20:40 UTC, then failed: `Publish-PSResource` |
|
||||
gave up waiting after 100 seconds while the Gallery accepted the upload, |
|
||||
and its retry got 409, so the job didn't create the GitHub release. The |
|
||||
published package passed the live tests of rc7 in both editions except |
|
||||
the one test whose expected warning text rc7 changed |
|
||||
(`Tests/Lab/Acceptance-2026-10-08-5.0.0-rc6.md`, After the release). |
|
||||
#116 (5.0.0-rc7) was opened on the rc6 branch and moved to `master`. |
|
||||
|
|
||||
## Stable capabilities |
## Stable capabilities |
||||
|
|
||||
- 36 cmdlets: access (7), audit (5), inheritance (6), owner and security |
- 36 cmdlets: access, audit, inheritance, owners/descriptors, privileges, |
||||
descriptor (4), privileges (3), long-path items (6), links, hash, and |
long-path items, links, hash, and disk space. |
||||
disk space (5). |
- Windows PowerShell 5.1 and PowerShell 7; RIPEMD160 and MACTripleDES are |
||||
- Works in Windows PowerShell 5.1 and PowerShell 7. In PowerShell 7, |
available only in Desktop. Both editions run elevated/basic-user in CI. |
||||
`Get-FileHash2` lacks `RIPEMD160` and `MACTripleDES`, which .NET lacks. |
- Pester fixtures use `Tests/TestHelpers.psm1` TEMP sandboxes. Live tests |
||||
- Pester tests in `Tests\` run in `$env:TEMP` sandboxes through |
are excluded from CI and run only on approved lab client/server targets. |
||||
`Tests\TestHelpers.psm1`; tests that need privileges skip without them |
|
||||
and run in CI, whose runners are elevated. |
|
||||
|
|
||||
## Open work |
## Open work |
||||
|
|
||||
1. Quality gate before 5.0.0 (Decision 21): the maintainer reruns the |
1. Decision 21 gate: review Decision 22, integrate reviewed quality-gate |
||||
failed Release job of 5.0.0-rc6, which creates the GitHub release; |
follow-up, publish the next candidate, and test the published package. |
||||
reviews the choices of Decision 22 in #116; merges #116 and tags |
Do not release 5.0.0 until the remaining-path and OS-matrix gates close. |
||||
5.0.0-rc7, whose published package then runs the live tests. Phase 3 |
Release steps: `Docs/Contributing/05-Releasing.md`; remove prerelease |
||||
runs the live tests on more operating systems. Then release 5.0.0 |
label, date `[5.0.0]`, update `$publishedVersions`, tag through CI. |
||||
through CI (Decision 12): remove the label, date |
2. Issues: #110's seven items were addressed by rc6, but #115 deliberately |
||||
`[Unreleased]` as `[5.0.0]`, add the last prerelease to |
used no closing keyword. #34 stays open for non-Windows owner feedback |
||||
`$publishedVersions`, and tag `5.0.0` (steps in |
or maintainer acceptance. #16, #21, #45, #89 await reporters. #68 tracks |
||||
`Docs/Contributing/05-Releasing.md`). #34 stays open with Bug and Help |
ShouldProcess for security cmdlets; enhancements #22/#49/#68/#77/#87 |
||||
Wanted until a tester with a file server that refuses the owner |
are not planned for 5.0.0. Labels follow Decision 17. |
||||
confirms the fix, or until 5.0.0 ships. |
3. Deferred reviews (not silently accepted): rc3 extra DACL read/SDDL |
||||
2. Issues: 5.0.0-rc6 addresses the seven items of #110 (tests); #115 |
snapshots/duplicate SACL check; rc4 findings listed in #113, including |
||||
named it without a closing keyword, so the maintainer closes it now. |
library-only RemoveAll account filters; rc5 unchecked Authz errors and |
||||
#21 (a misleading error of `Move-Item2`) got |
lab-controller hardening; rc6 failed privilege-disable retry (not |
||||
a fix in rc6 that names the missing destination folder; the folder |
reproduced); rc7 audit missing-path error IDs declined in Decision 22. |
||||
moves to another volume that rc7 fixes are a different defect. #68 |
4. Architecture/cmdlet design: Decision 22 remains proposed; two link |
||||
tracks `-WhatIf` and `-Confirm` for every cmdlet that changes security. |
changes are breaking. Keep unused classes/helper overloads until a |
||||
The labels follow Decision 17; #16, #21, #45, and #89 wait for their |
maintainer decision; do not remove them to improve coverage percentages. |
||||
reporters (Needs Info). Not planned for 5.0.0: the enhancements #22, |
5. ARM64 workstation: PowerShell 7.6.1 crashed under x64 emulation without |
||||
#49, #68, #77, #87. |
module frames; native-x64 CI did not reproduce it. |
||||
3. Review findings, not filed: of rc3, an extra DACL read and four SDDL |
6. Optional maintainer cleanup: obsolete AppVeyor/Read the Docs access, |
||||
snapshots on read paths, and a duplicate SACL check; of rc4, R3 to R5, |
wiki editing restrictions, `test/transfer`, and old lab checkpoints |
||||
R7, and five older defects, listed in the description of #113, among |
when no longer needed. No remote changes or snapshot restores here. |
||||
them the accounts filter that `RemoveFileSystemAccessRuleAll` and |
7. Fresh coverage inventory at `3442194`: 918 unvisited sequence points. |
||||
`RemoveFileSystemAuditRuleAll` ignore, which no cmdlet passes; of rc5, |
Of these, 244 are in classes unused by cmdlets and 112 in parameter |
||||
the bare `catch` in `Win32.GetEffectiveAccess`, the unchecked |
getters; 562 remain for finer review/testing, including unused overloads, |
||||
`AUTHZ_ACCESS_REPLY.Error`, and hardening of the lab controller (guards |
defensive/native failures, and environment-specific branches. High-value |
||||
in the setup blocks, interpolated `-EncodedCommand` paths, CredSSP by |
local gaps closed: folders/Force/DeleteError, RestoreOwnerError, all |
||||
IP address, the password string in memory, disabling the role accounts |
scopes, file/folder inheritance, enumeration/depth/link skipping, |
||||
after a run); of rc6, a privilege that fails to be disabled isn't tried |
descriptor write failures, and forced file replacement. Remaining |
||||
again by `Dispose` (finding 2, not reproducible); of rc7, one error ID |
candidates: audit ownership-retry failures, SD inheritance edge cases, |
||||
for a missing path in the audit cmdlets (declined, Decision 22). |
effective-access unresolved identity, recursive denial/error surfaces, |
||||
4. Behavior changes found in Phase 2 (Decision 16): decided in Decision 22 |
output-object comparisons/formatting. A conditional ACE display remains |
||||
as assumptions for the maintainer's review, on `ai/release-5.0.0-rc7`; |
a .NET representation limit, not evidence of unconditional permissions. |
||||
two of them are breaking changes of the link cmdlets. Left for Phase 3: |
8. Publication recovery is implemented locally in `95b827e`, with 14 offline |
||||
400 points of code that nothing calls besides the 244 lines of unused |
tests and exact artifact SHA-512 verification. Original upload errors |
||||
classes. |
remain errors for missing/different/unverifiable outcomes. Not deployed |
||||
5. `pwsh` 7.6.1 crashed three times during test runs on the ARM64 |
until the maintainer merges/pushes; no publication was performed here. |
||||
workstation (x64 emulation), without module frames; none of the CI runs |
9. Phase 3: choose OS scope (proposed Windows 11 client/2019/2022 servers), |
||||
on native x64 on 2026-10-05 crashed. |
detect ISO editions, provision without repurposing shared VMs, then run |
||||
6. Optional for the maintainer: delete the AppVeyor project and revoke its |
published-package acceptance. #34 has no new reply since 2026-10-06. |
||||
GitHub authorization, restrict wiki editing to collaborators, ask |
10. Lab rollback evidence: new checkpoints exist but report Standard even |
||||
`Sup3rlativ3` to delete the Read the Docs project, and delete the branch |
after a successful temporary ProductionOnly probe. Classification is |
||||
`test/transfer`. In the lab, delete the checkpoints |
unresolved; original VM policy restored, no checkpoint restored. Do |
||||
`ntfs-rc6-*-before-acceptance` and `ntfs-rc7-*-before-acceptance` of the |
not represent these as verified Production snapshots. |
||||
six machines when they are no longer needed. |
|
||||
7. Reachable code that no test runs (coverage report of rc6, ranked by |
|
||||
impact; about 300 points): `Remove-Item2` on folders (`-Recurse`, |
|
||||
`-Force`, `DeleteError`); the owner restore after taking ownership |
|
||||
(`RestoreOwnerError`); the inheritance cmdlets on folders and |
|
||||
`Set-NTFSInheritance -AccessInheritanceEnabled $true`; the mapping of |
|
||||
all 13 `-AppliesTo` values and the flag parameters of |
|
||||
`Remove-NTFSAccess`, `Add-NTFSAudit`, and `Remove-NTFSAudit`; the |
|
||||
switches and errors of `Get-ChildItem2`; the table views and |
|
||||
`InheritedFrom` in them; `Move-Item2 -Force`; account input errors; |
|
||||
`-PassThru` after success of the audit and inheritance cmdlets; |
|
||||
`Set-NTFSSecurityDescriptor` failures; the audit cmdlets without the |
|
||||
Security privilege on a local item; `Get-NTFSEffectiveAccess` for an |
|
||||
unresolvable SID; failed ownership retries of `Clear-NTFSAccess` and |
|
||||
`Set-NTFSInheritance`. A display limit, not a defect: a conditional ACE |
|
||||
shows as an unconditional entry, because the .NET rules have no |
|
||||
condition. |
|
||||
8. The publish step of the Release job fails when `Publish-PSResource` |
|
||||
gives up waiting after 100 seconds while the Gallery accepts the |
|
||||
package, because its retry gets 409 (5.0.0-rc6). Proposed for the |
|
||||
maintainer (Decision 16, not reproducible on demand; he was asked on |
|
||||
2026-10-08 and didn't answer, so it stays open): treat the error as |
|
||||
success when `Find-PSResource` then lists the version, in a script with |
|
||||
Pester tests. Until then, rerun the failed job. |
|
||||
|
|||||
@ -1,251 +1,156 @@ |
|||||
--- |
--- |
||||
status: current |
status: current |
||||
last-verified: 2026-10-08 |
last-verified: 2026-10-09 |
||||
owner: active-agent |
owner: active-agent |
||||
source: repository evidence |
source: repository and executable evidence |
||||
--- |
--- |
||||
|
|
||||
# Tech context |
# Tech context |
||||
|
|
||||
## Stack |
## Stack |
||||
|
|
||||
- C# class libraries, old-style `.csproj`, .NET Framework 4.5.2, |
- Legacy C# projects, .NET Framework 4.5.2, `NTFSSecurity.sln`. |
||||
solution `NTFSSecurity.sln` (Visual Studio 2017 format). |
Cmdlets depend on Security2, PrivilegeControl/ProcessPrivileges, and |
||||
- Projects: `NTFSSecurity` (cmdlets), `Security2` (ACL object model, Win32 |
AlphaFS 2.2.x. System.Management.Automation reference: 10.0.10586.0. |
||||
interop), `PrivilegeControl` and `ProcessPrivileges` (token privileges), |
- Module supports Windows PowerShell 5.1 and PowerShell 7; 36 cmdlets. |
||||
`Log`, `TestClient`, `NTFSSecurityTest` (MSTest, minimal coverage). |
Manifest initializes helper assemblies, aliases, type data, formatting, |
||||
- NuGet (`packages.config`): AlphaFS 2.2.x for long paths; |
and committed help generated from `Docs/Cmdlets` (platyPS 0.14/schema 2). |
||||
`System.Management.Automation.dll` 10.0.10586.0. For a drive or volume |
- CI: `.github/workflows/ci.yml`, scripts in `.github/scripts`; GitHub |
||||
root, AlphaFS `DirectoryInfo` reaches the device object, while |
renders Docs and publishes a generated wiki. No separate docs site. |
||||
`Directory.Get/SetAccessControl('C:\')` reaches the root folder (#41). |
|
||||
- Module: `NTFSSecurity.psd1` loads `NTFSSecurity.psm1` (aliases `dir2`, |
|
||||
`gi2`, `rm2`, `del2`), `NTFSSecurity.Init.ps1` (Add-Type of the helper |
|
||||
assemblies, prepends `NTFSSecurity.format.ps1xml`), and `NTFSSecurity.dll`. |
|
||||
- Documentation: Markdown in `Docs` and `README.md`, rendered by GitHub and |
|
||||
published to the wiki by CI; no documentation site (Decisions 9 and 11). |
|
||||
Cmdlet pages are platyPS 0.14 markdown (schema 2.0.0) in `Docs/Cmdlets`. |
|
||||
- Help: `NTFSSecurity\en-US\NTFSSecurity.dll-Help.xml`, generated from |
|
||||
`Docs/Cmdlets` and committed (Decision 8). |
|
||||
- Tests: Pester 5 in `Tests`, one file per area, against the Release |
|
||||
build; `Wiki.Tests.ps1` (wiki conversion) runs without a build. |
|
||||
- CI: GitHub Actions, `.github/workflows/ci.yml` with the scripts in |
|
||||
`.github/scripts` (Decision 11). |
|
||||
|
|
||||
## Environment |
## Current environment |
||||
|
|
||||
- Windows only (NTFS, Win32 security APIs). |
- Host `ExHost`: Windows Server 2025 VM, native x64, elevated agent; |
||||
- The Debug build writes straight into |
repository `V:\Git\NTFSSecurity`. AutomatedLab 5.61.704, Hyper-V, |
||||
`C:\Program Files\WindowsPowerShell\Modules\NTFSSecurity\`. |
approved lab `WindowsAccessControlLab` (Decision 20). |
||||
- No Visual Studio MSBuild or .NET Framework targeting pack on the |
- Build Release only: Debug writes to Program Files. Native .NET Framework |
||||
workstation. A local build works with the .NET Framework MSBuild |
MSBuild plus Roslyn `Microsoft.Net.Compilers` 4.2.0 and .NET 4.5.2 |
||||
(`%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe`) plus |
reference assemblies work; legacy compiler fails CS0136, dotnet MSBuild |
||||
`/p:CscToolPath` to the Roslyn `csc.exe` of the `Microsoft.Net.Compilers` |
fails binary resources MSB3822/MSB3823. Build packages are already cached |
||||
package; the legacy C# 5 compiler fails with CS0136. `dotnet msbuild` |
in `packages`; compiler/tools are under TEMP `ntfs-build`. |
||||
fails on the binary resources in `Resources.resx` (MSB3822, MSB3823). |
- Pester 5.7.1 is in |
||||
- platyPS 0.14.2, Pester 5.7.1, PSScriptAnalyzer, and powershell-yaml are |
`V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1`. |
||||
installed only for PowerShell 7. Windows PowerShell 5.1, started from |
platyPS 0.14.2 and MarkdownLinkCheck 0.2.0 are under TEMP `ntfs-docs-tools`. |
||||
PowerShell 7, imports platyPS and Pester by full path |
PSScriptAnalyzer and PSResourceGet are available in PowerShell 7. |
||||
(`~\OneDrive\Documents\PowerShell\Modules\platyPS\0.14.2`, |
- Use Desktop's module paths in Desktop children, not inherited Core-only |
||||
`C:\Program Files\PowerShell\Modules\Pester\5.7.1`). Leave |
paths. Never import NTFSSecurity in the agent shell; every package/build |
||||
`$env:PSModulePath` alone: PowerShell 7 hands the child the Windows |
runs in a new process. Current prereleases share assembly version 5.0.0.0. |
||||
PowerShell default path, and clearing it leaves Windows PowerShell without |
- GitHub CLI: `C:\Program Files\GitHub CLI\gh.exe`, signed in as raandree. |
||||
its core modules (Pester fails: `Add-Member` not found). |
Read-only queries work; remote mutations belong to the maintainer. |
||||
- MarkdownLinkCheck is not installed, and `Save-Module` crashed (FailFast) |
- LabSources: `V:\LabSources`. All 13 deployed machines are Server 2025. |
||||
in PowerShell 7.6 on 2026-10-04. Download the 0.2.0 package from |
Windows 11 consumer/enterprise-evaluation media and Server 2019/2022 |
||||
`https://www.powershellgallery.com/api/v2/package/MarkdownLinkCheck/0.2.0` |
ISO files exist. OS cache is empty; exact detected editions are not yet |
||||
into `$env:TEMP`, extract it, and import it by path. |
verified. Do not equate present media with a deployed/tested OS matrix. |
||||
- The first workstation is ARM64; PowerShell 7 runs as x64 under emulation. |
|
||||
- The NuGet cache (`~\.nuget\packages`) holds every build dependency: copy |
|
||||
`alphafs\2.2.1`, `system.management.automation.dll\10.0.10586`, and |
|
||||
`microsoft.netframework.referenceassemblies.net452\1.0.3` into |
|
||||
`packages\<Id>.<Version>`, and point `CscToolPath` at |
|
||||
`microsoft.net.compilers\4.2.0\tools`. |
|
||||
- The second workstation (x64, used since 2026-10-05) runs the agent |
|
||||
session elevated, so the tests that need privileges run there as in CI. |
|
||||
It has no NuGet cache with these packages: download each from |
|
||||
`https://api.nuget.org/v3-flatcontainer/<id>/<version>/<id>.<version>.nupkg`, |
|
||||
extract the first three into `packages\<Id>.<Version>` and the compilers |
|
||||
into `$env:TEMP`; Pester 5.7.1 comes from the Gallery package API the |
|
||||
same way, its folder first on `$env:PSModulePath` of the test process. |
|
||||
The GitHub CLI is in `C:\Program Files\GitHub CLI`, outside the PATH of |
|
||||
sessions started before its installation. |
|
||||
- The third workstation (`ExHost`, a Windows Server 2025 VM, x64, used since |
|
||||
2026-10-07) runs the agent session elevated and hosts the AutomatedLab lab |
|
||||
`WindowsAccessControlLab` (Decision 20) with Hyper-V and AutomatedLab |
|
||||
5.61.704. It has no NuGet cache or platyPS: check each |
|
||||
nuget.org package against the SHA-512 `packageHash` of its catalog entry |
|
||||
(`https://api.nuget.org/v3/registration5-semver1/<id>/<version>.json`, |
|
||||
then `catalogEntry`), and each Gallery package against `PackageHash` of |
|
||||
`api/v2/Packages(Id='<id>',Version='<version>')`. Pester 5.7.1 is in |
|
||||
`V:\Git\WindowsAccessControl\output\RequiredModules`. The GitHub CLI |
|
||||
2.102.0 is in `C:\Program Files\GitHub CLI`, outside the PATH, and signed |
|
||||
in as `raandree` since 2026-10-08; `Block-RemoteMutation` denies its |
|
||||
mutating commands, so the agent uses it read-only. The lab domains |
|
||||
`a.forest1.net` and `b.forest1.net` had a maximum password age of 42 |
|
||||
days, so the password of `install` expired on 2026-09-15 and AutomatedLab |
|
||||
got access denied; it never expires since 2026-10-07, as in |
|
||||
`forest1.net`. |
|
||||
|
|
||||
## Constraints |
## Constraints |
||||
|
|
||||
- `ModuleVersion` is `5.0.0` with the prerelease label `rc6` on the branch |
- Source manifest: ModuleVersion 5.0.0, prerelease rc7 on #116/follow-up. |
||||
`ai/release-5.0.0-rc6` (`rc5` on `master`). |
Latest stable: 4.2.6; latest published prerelease: rc6 (2026-10-08). |
||||
The latest stable tag and Gallery release is `4.2.6`. The manifest |
GitHub rc6 release recovered 2026-10-09. rc7 publication is pending. |
||||
requires PowerShell 5.1 and .NET Framework 4.5.2, uses `RootModule`, and |
- Changed-section writes preserve unchanged owner/group/DACL/SACL (19). |
||||
lists exactly 36 cmdlets; `Test-ModuleManifest` passes in Windows |
Roots use root-folder APIs, not AlphaFS device security (#41). |
||||
PowerShell 5.1 and PowerShell 7.6. |
- CHANGELOG contains user-visible changes only (7); tests and CI-only fixes |
||||
- The module source at `master` differs from tag `4.2.6` by the changes |
get no entry. No stable release until Decision 21 gates close. |
||||
that `CHANGELOG.md` lists under `[Unreleased]`, the release notes of each |
- Honor separate topic branches, no amendment, two AI co-author trailers. |
||||
5.0.0 prerelease. |
Never work around remote-mutation blocking. Provide each maintainer |
||||
- PowerShell Gallery versions (publish dates): 4.0.0 (2015-08-19), 4.2.2 |
command separately at reply end, no question dialog after commands. |
||||
(2016-05-18), 4.2.3 (2016-05-19), 4.2.4 (2018-08-13), 4.2.5 (2019-07-11), |
Issue references use no closing keyword unless closure is intended. |
||||
4.2.6 (2019-07-12), none with release notes; 5.0.0-rc1 (2026-10-04), |
- Lab passwords stay in memory and are lab-only; no secret in repository, |
||||
5.0.0-rc2 (2026-10-05), 5.0.0-rc3 and 5.0.0-rc4 (2026-10-06), 5.0.0-rc5 |
logs, or process arguments. Live ACL mutations occur only in the lab. |
||||
(2026-10-08), published |
- Existing expired installation passwords of a.forest1/b.forest1 were |
||||
by CI. Older versions were released on CodePlex only, and their dates are |
configured not to expire on 2026-10-07, matching the root domain. |
||||
lost. The git history starts on 2016-10-10, when the project moved from |
|
||||
CodePlex. |
|
||||
- Releases up to 4.2.6 were Debug builds published by hand, with the whole |
|
||||
output folder; their tags carry the previous version. From 5.0.0 on, CI |
|
||||
publishes on a version tag (Decision 12). GitHub releases attach |
|
||||
`NTFSSecurity.zip`. |
|
||||
- CI: GitHub Actions on pull requests, pushes to `master`, and version tags |
|
||||
(Decision 11); AppVeyor and Read the Docs aren't used (Decision 9). |
|
||||
- `CHANGELOG.md` lists user-visible changes only; CI and build-only changes |
|
||||
get no entry |
|
||||
([Decision 7](decisions/0007-changelog-user-visible-only.md)). |
|
||||
- Remote mutations are the maintainer's: the user-level preToolUse hook |
|
||||
`Block-RemoteMutation.ps1` denies `git push` and mutating `gh` commands |
|
||||
(`pr create`, `pr close`, and others) from the agent session, even after |
|
||||
an explicit request. Its override, `COPILOT_ATELIER_ALLOW_REMOTE=1`, is |
|
||||
read from the environment that VS Code starts the hook with; setting it |
|
||||
inside an agent command has no effect (verified 2026-10-04). The hook |
|
||||
matches the whole command text, so a commit message that quotes such a |
|
||||
command is blocked too. Prepare the commands and descriptions; the |
|
||||
maintainer runs them. Hand over each command as its own fenced code block |
|
||||
at the end of the reply, which the chat shows with a copy button, and end |
|
||||
the turn there; the maintainer reports back in the chat. The question |
|
||||
dialog joins the lines of its text, has no copy button, and covers the |
|
||||
reply before it (maintainer, 2026-10-06). A long question also hides its |
|
||||
choices, so that it can't be answered: keep it to a few short sentences |
|
||||
(2026-10-07). A pull request description |
|
||||
names an issue without a closing keyword (fixes, closes, resolves) unless |
|
||||
the merge should close it: "fixes #34" in #112 closed #34. Simulated `gh` |
|
||||
commands in offline tests must print what the real ones print, such as |
|
||||
the URL of a new comment. |
|
||||
|
|
||||
## Validation |
## Build and focused checks |
||||
|
|
||||
- CI (`.github/workflows/ci.yml`): job `build` on `windows-2025` installs |
- Build `NTFSSecurity\NTFSSecurity.csproj` with Configuration=Release, |
||||
platyPS 0.14.2, MarkdownLinkCheck 0.2.0, and Pester 5.7.1 for all users, |
Framework MSBuild, TargetFrameworkRootPath/FrameworkPathOverride to |
||||
restores `packages.config` per project plus |
`packages\Microsoft.NETFramework.ReferenceAssemblies.net452.1.0.3\build`, |
||||
`Microsoft.NETFramework.ReferenceAssemblies.net452` 1.0.3, builds |
CscToolPath to the cached compiler. Expected legacy CS1591/CS0618 warnings |
||||
`NTFSSecurity.csproj` in Release with the MSBuild that `vswhere` finds, |
are not new failures. Never copy a mutated DLL into acceptance artifacts. |
||||
then: 01 `Update-MarkdownHelp` and fail on `git diff -- Docs/Cmdlets`; 02 |
- Pester/builds run in detached monitored child processes through |
||||
`Get-MarkdownLink -BrokenOnly`; 03 regenerate the help file and fail on |
`Start-DetachedPowerShell.ps1`; use unique TEMP logs/result paths and an |
||||
`git status --porcelain -- NTFSSecurity/en-US`; 04 `Invoke-Tests.ps1` in |
explicit-PID watcher. No foreground sleep/poll loop. Long payloads use |
||||
Windows PowerShell 5.1 and in PowerShell 7, then |
a script file: nested Base64 encoding can exceed Windows command limits. |
||||
`Invoke-TestsAsBasicUser.ps1` in both editions (since 5.0.0-rc6). Job |
- Focused helper: TEMP `ntfs-focused\Start-FocusedRuns.ps1`; detach that |
||||
`wiki` on `ubuntu-latest` |
driver too because its internal wait loop must not block the agent shell. |
||||
(read-only) clones the wiki (`gh auth setup-git` with the built-in token), |
- TEMP `ntfs-docs-tools\Invoke-ChangeChecks.ps1 -File <relative paths>` |
||||
runs `Export-WikiContent.ps1`, and lists the changed pages in the job |
performs AST/analyzer/lint/help checks. Absolute input paths misroute |
||||
summary; job `publish-wiki` (`contents: write`) repeats that and publishes, |
cmdlet pages. Check actual analyzer/lint output, not just helper exit. |
||||
for `master` only. After the tests, `build` runs |
- actionlint 1.7.12 checks the workflow. Script changes use AST parse and |
||||
`New-ModulePackage.ps1` and uploads the artifact `packages` (nupkg and |
PSScriptAnalyzer; prose Markdown uses MD013 and changelog siblings-only |
||||
`NTFSSecurity.zip`). Job `release` runs only for tags matching |
repeated-heading allowance. Native error codes must be checked explicitly. |
||||
`[0-9]+.[0-9]+.[0-9]+` or `[0-9]+.[0-9]+.[0-9]+-*`, in the environment |
- Documentation: run platyPS in Desktop, generate external help, rebuild, |
||||
`powershell-gallery` (secret `PSGALLERY_API_KEY`); see Decision 12. |
require an unchanged Markdown round trip. Links in Docs are checked |
||||
Actions are pinned by commit SHA: `actions/checkout` v7.0.1, |
relatively; Wiki tests cover generated anchors, not arbitrary web URLs. |
||||
`actions/upload-artifact` v7.0.1, `actions/download-artifact` v8.0.1; |
|
||||
Dependabot proposes updates weekly, one week after a release. |
## CI and packaging |
||||
- Packaging needs PSResourceGet (`Compress-PSResource`, PowerShell 7.4 or |
|
||||
later); its tests skip in Windows PowerShell. Dry run locally: run |
- CI `build` on windows-2025 installs tools, restores dependencies, builds |
||||
`New-ModulePackage.ps1` against `NTFSSecurity\bin\Release` into |
Release, round-trips pages/help, checks links, and runs the suite in |
||||
`$env:TEMP`, then extract the nupkg into a folder and import it there. |
Desktop/Core, elevated/basic user. Lab tests are explicitly excluded. |
||||
- Read CI runs with `gh run list --repo raandree/NTFSSecurity --workflow |
- `.github/scripts/Invoke-TestsAsBasicUser.ps1` launches a SAFER Normal User |
||||
ci.yml`, `gh pr checks <number>`, and `gh run view <id> --log-failed` |
token. Result paths may be absolute or repository-relative: Path.Combine |
||||
(read-only). |
then GetFullPath, not Join-Path with a rooted child. |
||||
- Workflow lint: actionlint (download the release zip into `$env:TEMP` and |
- Packaging needs Compress-PSResource (Core 7.4+). New-ModulePackage copies |
||||
check its SHA-256 against the checksum file; 1.7.12 on 2026-10-08); |
only FileList, validates the manifest, creates nupkg plus NTFSSecurity.zip. |
||||
PowerShell steps check |
Check package/file hashes and test the extracted artifact, not build extras. |
||||
`$LASTEXITCODE` after every native command, because GitHub checks only |
- Release runs only for validated version tags in powershell-gallery. |
||||
the last one. |
API key stays as PSGALLERY_API_KEY environment reference. Helper |
||||
- Run platyPS in Windows PowerShell 5.1 to avoid PowerShell 7.4+ |
Publish-ModulePackage treats only PackageNotFound as expected absence; |
||||
`-ProgressAction` noise. |
existing-version skip and uncertain-upload recovery require exact Gallery |
||||
- Placeholder check: no `{{` left in `Docs/Cmdlets/*.md`. |
SHA-512 equality. Base64 comparison is case-sensitive. Unverifiable, |
||||
- Help file: `New-ExternalHelp -Path .\Docs\Cmdlets -OutputPath |
missing, and different outcomes preserve errors. No test uploads. |
||||
.\NTFSSecurity\en-US -Force` must leave `git status` unchanged. |
- Read status through gh pr checks / gh run view --log-failed. A successful |
||||
- Pester: run detached (`Start-DetachedPowerShell.ps1`) in Windows |
Gallery upload followed by HTTP 409 does not prove its retry chronology. |
||||
PowerShell 5.1: the launcher starts `pwsh`, and its payload runs |
|
||||
`powershell.exe -NoProfile -EncodedCommand` with Pester imported by full |
## Coverage and test eligibility |
||||
path. A run without `bin\Release\en-US` must fail. |
|
||||
- Tests that run only without a privilege skip in an elevated session. |
- AltCover 9.0.145 net472 instruments a copied Release build with PDBs, |
||||
`.github\scripts\Invoke-TestsAsBasicUser.ps1` runs the suite from an |
OpenCover format, localSource, excluding AlphaFS/System.Management.Automation. |
||||
elevated session with a token of the SAFER level Normal User, like |
Do not use --save: collection previously retained only one process's hits. |
||||
`runas /trustlevel:0x20000`, and CI runs it in both editions. For a |
- Freeze a git worktree, instrument its NTFSSecurity\bin\Release, run all |
||||
single file, `runas /trustlevel:0x20000` works too; give Windows |
four configurations sequentially with the real CI wrappers, then |
||||
PowerShell its own `PSModulePath`, and note that `runas` returns at once, |
AltCover runner --collect recalculates the report. Compute option paths |
||||
so the script it starts writes its own log. Both tokens hold only the |
before passing native arguments, not inline Join-Path expressions. |
||||
privilege to bypass traverse checking. |
- Report sequence points, not unique source lines. Four-run baselines: |
||||
Pester reports a skipped `-ForEach` test under its template name, such as |
rc5 2,020/3,476 (58.1%), branches 711/1,873 (38.0%); |
||||
`<_> should ...`, and a test that ran under the expanded name: compare |
rc6 2,412/3,540 (68.14%), branches 850/1,918 (44.32%); |
||||
runs by template. |
follow-up `3442194` 2,641/3,559 (74.21%), 974/1,933 (50.39%). |
||||
- C# coverage (Decision 21): AltCover 9.0.145 (`tools\net472\AltCover.exe` |
NTFSSecurity assembly: 1,769/2,099 (84.28%). Different code changes |
||||
of the nuget.org package) instruments a copy of the local Release build, |
denominators; never present these as same-source incremental percentages. |
||||
which has the PDB files that the published package lacks: |
- Final suite: 914 per configuration, zero failures. Passed/skipped: |
||||
`--reportFormat=OpenCover`, AlphaFS and `System.Management.Automation` |
elevated Desktop 890/24, Core 860/54; basic Desktop 749/165, Core 719/195. |
||||
excluded with `--assemblyFilter`, and no `--save`: then every process |
- NUnit skipped ForEach names retain placeholders and parameter tuples, |
||||
writes its hits into the report when it exits. With `--save`, each |
executed names expand them. Strip trailing data tuples and match templates; |
||||
process writes a recorder file, and `runner --collect` keeps only the |
raw-name intersection or positional alignment is invalid across editions. |
||||
first one (verified 2026-10-08), so the numbers measured that way held |
139 skipped templates have eligible executed counterparts. Inspect input |
||||
only the main process of the elevated Windows PowerShell run. Put the |
eligibility when an individual data row has a condition of its own. |
||||
instrumented module in `NTFSSecurity\bin\Release` of a `git worktree`, |
- Remaining inventory: 918 points, including 244 in cmdlet-unused classes, |
||||
run `.github\scripts\Invoke-Tests.ps1` elevated and |
112 parameter-getter points, and 562 awaiting finer classification/testing. |
||||
`Invoke-TestsAsBasicUser.ps1` in both editions, then |
Preserve raw XML, eligibility CSV, logs, commit identity, and build hashes. |
||||
`AltCover.exe runner --collect --recorderDirectory=<the instrumented |
|
||||
folder>`, which recalculates the summary of the report from the hits. |
## Lab acceptance |
||||
All four configurations, 2026-10-08: the rc5 tree 58.1% of the lines |
|
||||
(2,020 of 3,476) and 38.0% of the branches (711 of 1,873), 62.5% without |
- Defaults: F1ADC1 (domain), F1AFile2 (server), F1AFile1 (client), all in |
||||
244 lines in classes that no cmdlet calls; the rc6 candidate (`1b9edbb`) |
a.forest1.net. Foreign accounts use F1BDC1, F2DC1, F3DC1 and existing trusts. |
||||
68.1% of the lines (2,412 of 3,540) and 44.3% of the branches (850 of |
Controller accepts alternate machines; changing topology/OS scope waits |
||||
1,918), 73.2% without those classes, the `NTFSSecurity` assembly 78.1%. |
for a maintainer decision. Do not repurpose another project's shared VMs. |
||||
The earlier figures, 55.9% for rc5 and 65.6% for rc6, used `--save`. |
- Before a run: authenticated WinRM, LDAP RootDSE, Kerberos tickets, member |
||||
- Live tests (Decision 20): in an elevated Windows PowerShell 5.1 session |
secure channels, clocks; checkpoint only approved targets. Inspect actual |
||||
on the lab host, `Tests\Lab\Invoke-NTFSSecurityLabTest.ps1` with |
checkpoint kind: new checkpoints reported Standard even after a successful |
||||
`-Version` for Gallery packages or `-ModulePath` for a build; it writes |
temporary ProductionOnly request. Policy restored; no rollback performed; |
||||
the results to `$env:TEMP\NTFSSecurityLab\Results`. A run of two versions |
Production classification remains unverified, not a passed safety check. |
||||
in both editions takes about 30 minutes; `-RemoveFixture` removes its |
- Run Tests\Lab\Invoke-NTFSSecurityLabTest.ps1 in elevated Desktop with |
||||
accounts, share, and folders from the lab. For a check on the client as |
-Version for hash-checked Gallery packages or -ModulePath for the extracted |
||||
an account without administrator rights, use `NtfsLiveServerAdmin` |
build artifact, both editions. Per version/edition: Delegate, ServerAdmin, |
||||
(Remote Management Users on the client, CredSSP by IP address like the |
Admin on client, then Server independently checks persisted state. |
||||
controller): reset its password on the PDC emulator to a random value |
- Controller writes Summary.json even when tests fail: validate every role, |
||||
in memory; the next run of the controller sets a new one anyway. |
exit code, failure name, and total; DONE alone is not acceptance evidence. |
||||
- Lab acceptance of a candidate (modeled on the WindowsAccessControl |
Desktop ConvertFrom-Json can wrap arrays; explicitly enumerate the result |
||||
handoff 07): build once, package it with `New-ModulePackage.ps1`, and |
and compare full Describe-prefixed names. Never gate cleanup on the global |
||||
record the SHA-256 of the packages and module files; check WinRM, LDAP |
Error.Count, which includes handled errors; independently verify footprint. |
||||
(RootDSE), Kerberos (`klist get`), the secure channel, and the clock of |
- Remote Authz answers administrators and Access Control Assistance |
||||
the six VMs; take a Production checkpoint named |
Operators (S-1-5-32-579); other accounts get access denied. Check firewall |
||||
`ntfs-<label>-<commit>-before-acceptance` of `F1ADC1`, `F1BDC1`, |
when remote resource-manager RPC fails. Expected rights use S4U tokens. |
||||
`F2DC1`, `F3DC1`, `F1AFile1`, and `F1AFile2`; run the controller with |
- RemoveFixture after the run; verify OUs/accounts, share, folders, local |
||||
`-ModulePath` of the extracted `NTFSSecurity.zip` in both editions; then |
memberships, and test profiles removed. Credentials must never be printed. |
||||
`-RemoveFixture` and check that the accounts, share, folders, group |
|
||||
memberships, and profiles are gone. |
|
||||
- `Get-NTFSEffectiveAccess -ServerName`: the authorization manager of the |
|
||||
named computer answers only its administrators and the members of its |
|
||||
group Access Control Assistance Operators (S-1-5-32-579); others get |
|
||||
"Access is denied" (5). Lab probe of 2026-10-08 on `F1AFile2`. |
|
||||
- Markdown lint: `npx markdownlint-cli2` with `MD013` limited to prose |
|
||||
(tables, code, and headings excluded) on the conceptual pages; for |
|
||||
`CHANGELOG.md` also `MD024` with `siblings_only: true`, because every |
|
||||
version repeats the category headings. |
|
||||
- Gallery packages: download |
|
||||
`https://www.powershellgallery.com/api/v2/package/NTFSSecurity/<version>` |
|
||||
into `$env:TEMP` and extract it; dates come from the OData endpoint |
|
||||
`api/v2/FindPackagesById()?id='NTFSSecurity'`. Import each version in its |
|
||||
own process: every version's `NTFSSecurity.dll` has assembly version |
|
||||
4.2.1.0, so a second version in the same process reuses the first DLL. |
|
||||
- YAML: `ConvertFrom-Yaml` (powershell-yaml) on `.github/workflows/ci.yml`. |
|
||||
- Links: the CI step 02 (MarkdownLinkCheck 0.2.0) checks only relative |
|
||||
links in `Docs`; it strips anchors and skips absolute URLs. |
|
||||
`Wiki.Tests.ps1` checks the wiki links with their anchors; check the |
|
||||
links in `README.md` and `CHANGELOG.md` with a script. |
|
||||
|
|||||
@ -0,0 +1,81 @@ |
|||||
|
<# |
||||
|
Tests the basic-user CI wrapper without creating a process or changing privileges. A fake native process writes |
||||
|
the same result-file boundary as the child; only the Add-Type call of the copied wrapper is mocked. |
||||
|
#> |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
||||
|
)] |
||||
|
param () |
||||
|
|
||||
|
BeforeAll { |
||||
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
||||
|
$sandbox = New-TestSandbox -Name 'BasicUserWrapper' |
||||
|
$repository = Join-Path -Path $sandbox -ChildPath 'Repository' |
||||
|
$scripts = Join-Path -Path $repository -ChildPath '.github\scripts' |
||||
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $scripts |
||||
|
New-Item -ItemType Directory -Path $scripts -Force | Out-Null |
||||
|
$wrapper = Join-Path -Path $scripts -ChildPath 'Invoke-TestsAsBasicUser.ps1' |
||||
|
Copy-Item -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\Invoke-TestsAsBasicUser.ps1') -Destination $wrapper |
||||
|
Add-Type -TypeDefinition @" |
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Text.RegularExpressions; |
||||
|
|
||||
|
public static class NTFSSecurityBasicUserProcess |
||||
|
{ |
||||
|
public static int Calls; |
||||
|
public static string WorkingDirectory; |
||||
|
|
||||
|
public static int Run(string applicationName, string commandLine, string currentDirectory) |
||||
|
{ |
||||
|
Calls++; |
||||
|
WorkingDirectory = currentDirectory; |
||||
|
var match = Regex.Match(commandLine, "-ResultPath \"([^\"]+)\""); |
||||
|
if (!match.Success) |
||||
|
throw new InvalidOperationException("The child command has no result path."); |
||||
|
File.WriteAllText(match.Groups[1].Value, "<test-results />"); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
"@ |
||||
|
} |
||||
|
|
||||
|
AfterAll { |
||||
|
Remove-TestSandbox -Sandbox $sandbox |
||||
|
} |
||||
|
|
||||
|
Describe 'Invoke-TestsAsBasicUser.ps1 result paths' { |
||||
|
BeforeEach { |
||||
|
[NTFSSecurityBasicUserProcess]::Calls = 0 |
||||
|
[NTFSSecurityBasicUserProcess]::WorkingDirectory = $null |
||||
|
Mock -CommandName Add-Type -ParameterFilter { $TypeDefinition -like '*class NTFSSecurityBasicUserProcess*' } |
||||
|
} |
||||
|
|
||||
|
It 'Should copy the result to an absolute path, also when that path contains spaces' { |
||||
|
$result = Join-Path -Path $sandbox -ChildPath 'Absolute results\Result.xml' |
||||
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $result |
||||
|
|
||||
|
& $wrapper -ResultPath $result -Title 'Absolute result path' | Out-Null |
||||
|
|
||||
|
Get-Content -LiteralPath $result -Raw | Should -BeExactly '<test-results />' |
||||
|
[NTFSSecurityBasicUserProcess]::Calls | Should -Be 1 |
||||
|
[NTFSSecurityBasicUserProcess]::WorkingDirectory | Should -Be $repository |
||||
|
Should -Invoke -CommandName Add-Type -Times 1 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should resolve a relative path against the repository, not the caller location' { |
||||
|
$result = Join-Path -Path $repository -ChildPath 'Relative results\Result.xml' |
||||
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $result |
||||
|
Push-Location -LiteralPath $sandbox |
||||
|
try { |
||||
|
& $wrapper -ResultPath 'Relative results\Result.xml' -Title 'Relative result path' | Out-Null |
||||
|
} |
||||
|
finally { |
||||
|
Pop-Location |
||||
|
} |
||||
|
|
||||
|
Get-Content -LiteralPath $result -Raw | Should -BeExactly '<test-results />' |
||||
|
[NTFSSecurityBasicUserProcess]::Calls | Should -Be 1 |
||||
|
[NTFSSecurityBasicUserProcess]::WorkingDirectory | Should -Be $repository |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
# Quality-gate follow-up acceptance, 2026-10-09 |
||||
|
|
||||
|
Further validation of NTFSSecurity before 5.0.0, on |
||||
|
`ai/quality-gate-coverage`, based on rc7 PR #116 (`d25647d`). This is a |
||||
|
local candidate, not a published release or a claim that the quality gate |
||||
|
is complete. Architecture and cmdlet-design choices remain with the |
||||
|
maintainer (Decisions 16, 21, and 22). |
||||
|
|
||||
|
## Candidate and artifact identity |
||||
|
|
||||
|
- Module code/test baseline: `3442194`; first-hidden-item fix: `d610372`. |
||||
|
- Build: Release, .NET Framework 4.5.2; manifest label `5.0.0-rc7`. |
||||
|
The label has not been advanced or published by this work. |
||||
|
- Packages produced by `.github/scripts/New-ModulePackage.ps1`. |
||||
|
All 11 files in the live-tested packaged module folder match the |
||||
|
extracted `NTFSSecurity.zip` byte-for-byte by SHA-256. |
||||
|
- Package SHA-256 values: |
||||
|
|
||||
|
| Artifact | SHA-256 | |
||||
|
| --- | --- | |
||||
|
| `NTFSSecurity.5.0.0-rc7.nupkg` | `E76B80CA8CBCD4E46EB5B4C61E53BD0BFCB461881593753A69F7F90385533768` | |
||||
|
| `NTFSSecurity.zip` | `ACA302594BF0B84EAF6F4E45476DC4AA096F5F9105E51B1F255FB061D57E99EC` | |
||||
|
| `NTFSSecurity.dll` | `4587F1B2FCF2683B02D1895CEE17D0ABF4060DA758264E09AEC0CF62536E7CAC` | |
||||
|
|
||||
|
## Local validation |
||||
|
|
||||
|
Final uninstrumented suite, 09:31 to 09:34 UTC; separate processes with the |
||||
|
real CI elevated/basic-user wrappers. Every configuration discovered 914 |
||||
|
cases (202 above rc7); no test failed. Every skipped test template has an |
||||
|
executed counterpart in the four-run matrix. NUnit skipped data names were |
||||
|
normalized, not compared positionally or as literal expanded names. |
||||
|
|
||||
|
| Configuration | Passed | Failed | Skipped | Total | |
||||
|
| --- | ---: | ---: | ---: | ---: | |
||||
|
| Windows PowerShell 5.1, elevated | 890 | 0 | 24 | 914 | |
||||
|
| Windows PowerShell 5.1, basic user | 749 | 0 | 165 | 914 | |
||||
|
| PowerShell 7, elevated | 860 | 0 | 54 | 914 | |
||||
|
| PowerShell 7, basic user | 719 | 0 | 195 | 914 | |
||||
|
|
||||
|
AST parse and PSScriptAnalyzer: zero issues in all 13 changed PowerShell |
||||
|
files. Release build, actionlint, Markdown lint, help generation/round trip, |
||||
|
and relative documentation links passed. Existing compiler warnings were |
||||
|
retained, not suppressed to obtain a green result. |
||||
|
|
||||
|
New guards cover folder deletion, read-only/locked/junction/long-path |
||||
|
cases, owner restoration/retry failures, all 13 permission scopes in both |
||||
|
forms and storage modes, descendant propagation, file/folder inheritance, |
||||
|
enumeration/filter/depth/link skipping, forced replacement, and descriptor |
||||
|
write failure/continuation. Controlled mutations made the relevant tests |
||||
|
fail; source was restored exactly and Release rebuilt before validation. |
||||
|
|
||||
|
Reproduced product defect: `Get-ChildItem2 -Hidden` omitted the first |
||||
|
hidden item because implied Force was set after deciding whether to emit |
||||
|
it. The regression failed before the fix in all four configurations. |
||||
|
CI result paths were also fixed test-first. Publication recovery has 14 |
||||
|
offline tests; no real upload occurred. |
||||
|
|
||||
|
## Coverage method and remaining inventory |
||||
|
|
||||
|
AltCover 9.0.145 OpenCover report of frozen `3442194`, 09:24 to 09:28 UTC: |
||||
|
all four configurations sequentially, no `--save`, copied Release PDBs, |
||||
|
AlphaFS and System.Management.Automation excluded. Percentages are visited |
||||
|
sequence/branch points divided by their respective totals, not unique |
||||
|
source-line coverage. |
||||
|
|
||||
|
| Assembly | Sequence points | Sequence coverage | Branch points | Branch coverage | |
||||
|
| --- | ---: | ---: | ---: | ---: | |
||||
|
| NTFSSecurity | 1,769/2,099 | 84.28% | 605/1,051 | 57.56% | |
||||
|
| Security2 | 747/1,219 | 61.28% | 330/740 | 44.59% | |
||||
|
| ProcessPrivileges | 113/219 | 51.60% | 35/125 | 28.00% | |
||||
|
| PrivilegeControl | 12/22 | 54.55% | 4/17 | 23.53% | |
||||
|
| Aggregate | 2,641/3,559 | 74.21% | 974/1,933 | 50.39% | |
||||
|
|
||||
|
rc6 aggregate was 68.14% sequence/44.32% branch coverage. Its production |
||||
|
code differs, so the denominators differ. The 918 unvisited points remain |
||||
|
visible: 244 in classes unused by cmdlets, 112 parameter-getter points, |
||||
|
and 562 for finer classification/testing (unused overloads, defensive, |
||||
|
environment-specific, and reachable paths). This is not "everything tested |
||||
|
or explained" yet. For example, code intelligence finds only the definition |
||||
|
of `MapGenericRightsToFileSystemRights`, not a caller; do not test/remove an |
||||
|
unused helper merely to improve a percentage. |
||||
|
|
||||
|
## Lab and rollback evidence |
||||
|
|
||||
|
`WindowsAccessControlLab`: F1ADC1, F1BDC1, F2DC1, F3DC1, F1AFile1 (client), |
||||
|
and F1AFile2 (file server), all Windows Server 2025. Before the run, |
||||
|
authenticated WinRM, LDAP RootDSE, Kerberos tickets, member secure channels, |
||||
|
and clocks passed. No VM topology or operating system was changed. |
||||
|
|
||||
|
Six checkpoints named `ntfs-qg-3442194-before-acceptance` exist. Hyper-V |
||||
|
reports them as Standard. A temporary ProductionOnly request on F1AFile1 |
||||
|
also succeeded but reported Standard; its original policy was restored. |
||||
|
Production classification remains unverified. No checkpoint was restored; |
||||
|
these are not verified Production rollback evidence. |
||||
|
|
||||
|
## Live results |
||||
|
|
||||
|
Controller ran from 09:20 to 09:51 UTC against hash-checked published rc6 |
||||
|
and the candidate, each version/edition in new processes. The new fixture |
||||
|
contains exactly one hidden file: the Delegate lists it over SMB with |
||||
|
`-Hidden` alone, and the Server verifies its content/attribute independently. |
||||
|
|
||||
|
| Version | Edition | Role | Passed | Failed | Skipped | |
||||
|
| --- | --- | --- | ---: | ---: | ---: | |
||||
|
| Candidate | Desktop | Delegate | 39 | 0 | 0 | |
||||
|
| Candidate | Desktop | ServerAdmin | 13 | 0 | 0 | |
||||
|
| Candidate | Desktop | Admin | 40 | 0 | 0 | |
||||
|
| Candidate | Desktop | Server | 73 | 0 | 1 | |
||||
|
| Candidate | Core | Delegate | 39 | 0 | 0 | |
||||
|
| Candidate | Core | ServerAdmin | 13 | 0 | 0 | |
||||
|
| Candidate | Core | Admin | 40 | 0 | 0 | |
||||
|
| Candidate | Core | Server | 73 | 0 | 1 | |
||||
|
| Published rc6 | Each edition | Delegate | 38 | 1 | 0 | |
||||
|
| Published rc6 | Each edition | ServerAdmin | 13 | 0 | 0 | |
||||
|
| Published rc6 | Each edition | Admin | 39 | 1 | 0 | |
||||
|
| Published rc6 | Each edition | Server | 73 | 0 | 1 | |
||||
|
|
||||
|
Candidate aggregate: 330 passed, zero failed, two expected skips (Server |
||||
|
does not import the module). rc6 aggregate: 326 passed, four expected |
||||
|
failures, two skips. The only rc6 failures are Hidden and the already-known |
||||
|
rc7 effective-access warning-text expectation, once each per edition. |
||||
|
|
||||
|
The temporary host verifier initially failed because Desktop wrapped the |
||||
|
JSON array and failure names included Describe prefixes. A corrected |
||||
|
verifier flattened the array, checked all 16 unique version/edition/role |
||||
|
results and exact failure names, and passed in both editions on the |
||||
|
unchanged results. Original raw logs/exit markers were preserved. |
||||
|
|
||||
|
## Cleanup and review |
||||
|
|
||||
|
RemoveFixture ran at 09:57 UTC. An overly broad host Error.Count check gave |
||||
|
its wrapper a failing marker despite the controller completing. Independent |
||||
|
read-only probes verified on all six machines: zero test OUs/users/groups, |
||||
|
no share or fixture folders, no test local-group memberships, no test |
||||
|
profiles. Cleanup is proved by end state, not that wrapper marker. |
||||
|
|
||||
|
One independent read-only code review approved the diff with high |
||||
|
confidence and no significant issues or confirmed exploitable vulnerability. |
||||
|
The custom security-reviewer could not start because its configured model |
||||
|
was unavailable; the built-in code-review agent performed the one review. |
||||
|
No source changed after that pass; result-verifier repairs were temporary |
||||
|
host tooling only. |
||||
|
|
||||
|
## Evidence and remaining release gates |
||||
|
|
||||
|
Raw coverage XML, eligibility/inventory CSVs, final suite XML/logs, |
||||
|
mutation logs, module/package hashes, full live role results, original host |
||||
|
logs, corrected verification, and independent cleanup evidence are retained |
||||
|
in the session artifact `quality-gate-3442194-20261009`. |
||||
|
|
||||
|
Remaining: finer uncovered-path inventory, Decision 22 review, integration |
||||
|
and CI of this branch, publication and published-package acceptance, wider |
||||
|
OS matrix, and non-Windows #34 feedback. All 13 deployed lab VMs remain |
||||
|
Server 2025. Windows 11/Server 2019/2022 ISO media exists, but detected |
||||
|
editions/OS cache and matrix scope are not yet verified. #34 has no reply |
||||
|
since 2026-10-06. Do not release stable 5.0.0 on the strength of this record. |
||||
@ -0,0 +1,165 @@ |
|||||
|
<# |
||||
|
Tests all permission scopes through the access and audit cmdlets, both parameter forms and both storage modes. |
||||
|
Expected flags are the Windows ACE flags, independent of the module's scope converter. |
||||
|
#> |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
||||
|
)] |
||||
|
param () |
||||
|
|
||||
|
BeforeDiscovery { |
||||
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
||||
|
$canReadAudit = Test-PrivilegeHeld -Name 'SeSecurityPrivilege' |
||||
|
$scopes = @( |
||||
|
@{ Name = 'ThisFolderOnly'; Inheritance = 'None'; Propagation = 'None' } |
||||
|
@{ Name = 'ThisFolderSubfoldersAndFiles'; Inheritance = 'ContainerInherit, ObjectInherit'; Propagation = 'None' } |
||||
|
@{ Name = 'ThisFolderAndSubfolders'; Inheritance = 'ContainerInherit'; Propagation = 'None' } |
||||
|
@{ Name = 'ThisFolderAndFiles'; Inheritance = 'ObjectInherit'; Propagation = 'None' } |
||||
|
@{ Name = 'SubfoldersAndFilesOnly'; Inheritance = 'ContainerInherit, ObjectInherit'; Propagation = 'InheritOnly' } |
||||
|
@{ Name = 'SubfoldersOnly'; Inheritance = 'ContainerInherit'; Propagation = 'InheritOnly' } |
||||
|
@{ Name = 'FilesOnly'; Inheritance = 'ObjectInherit'; Propagation = 'InheritOnly' } |
||||
|
@{ Name = 'ThisFolderSubfoldersAndFilesOneLevel'; Inheritance = 'ContainerInherit, ObjectInherit'; Propagation = 'NoPropagateInherit' } |
||||
|
@{ Name = 'ThisFolderAndSubfoldersOneLevel'; Inheritance = 'ContainerInherit'; Propagation = 'NoPropagateInherit' } |
||||
|
@{ Name = 'ThisFolderAndFilesOneLevel'; Inheritance = 'ObjectInherit'; Propagation = 'NoPropagateInherit' } |
||||
|
@{ Name = 'SubfoldersAndFilesOnlyOneLevel'; Inheritance = 'ContainerInherit, ObjectInherit'; Propagation = 'InheritOnly, NoPropagateInherit' } |
||||
|
@{ Name = 'SubfoldersOnlyOneLevel'; Inheritance = 'ContainerInherit'; Propagation = 'InheritOnly, NoPropagateInherit' } |
||||
|
@{ Name = 'FilesOnlyOneLevel'; Inheritance = 'ObjectInherit'; Propagation = 'InheritOnly, NoPropagateInherit' } |
||||
|
) |
||||
|
$activeTargets = @{ |
||||
|
ThisFolderOnly = @('Root') |
||||
|
ThisFolderSubfoldersAndFiles = @('Root', 'File', 'Child', 'ChildFile', 'Grandchild', 'GrandchildFile') |
||||
|
ThisFolderAndSubfolders = @('Root', 'Child', 'Grandchild') |
||||
|
ThisFolderAndFiles = @('Root', 'File', 'ChildFile', 'GrandchildFile') |
||||
|
SubfoldersAndFilesOnly = @('File', 'Child', 'ChildFile', 'Grandchild', 'GrandchildFile') |
||||
|
SubfoldersOnly = @('Child', 'Grandchild') |
||||
|
FilesOnly = @('File', 'ChildFile', 'GrandchildFile') |
||||
|
ThisFolderSubfoldersAndFilesOneLevel = @('Root', 'File', 'Child') |
||||
|
ThisFolderAndSubfoldersOneLevel = @('Root', 'Child') |
||||
|
ThisFolderAndFilesOneLevel = @('Root', 'File') |
||||
|
SubfoldersAndFilesOnlyOneLevel = @('File', 'Child') |
||||
|
SubfoldersOnlyOneLevel = @('Child') |
||||
|
FilesOnlyOneLevel = @('File') |
||||
|
} |
||||
|
$propagationCases = @($scopes | ForEach-Object { @{ Name = $_.Name; ActiveTargets = $activeTargets[$_.Name] } }) |
||||
|
$scopeNames = @($scopes.Name) |
||||
|
$scopeCases = @(foreach ($scope in $scopes) { |
||||
|
foreach ($source in 'Path', 'SecurityDescriptor') { |
||||
|
foreach ($form in 'AppliesTo', 'Flags') { |
||||
|
@{ Name = $scope.Name; Inheritance = $scope.Inheritance; Propagation = $scope.Propagation; Source = $source; Form = $form } |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
BeforeAll { |
||||
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
||||
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1') -Force -ErrorAction Stop |
||||
|
$sandbox = New-TestSandbox -Name 'PermissionScopes' |
||||
|
Push-Location -LiteralPath $sandbox |
||||
|
$account = 'S-1-5-21-1-2-3-4801' |
||||
|
$keeper = 'S-1-5-21-1-2-3-4802' |
||||
|
} |
||||
|
|
||||
|
AfterAll { |
||||
|
Pop-Location |
||||
|
Remove-TestSandbox -Sandbox $sandbox |
||||
|
Remove-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue |
||||
|
} |
||||
|
|
||||
|
Describe 'Permission scope inventory' { |
||||
|
It 'Should cover every named -AppliesTo value' -ForEach @(@{ ScopeNames = $scopeNames }) { |
||||
|
(@([Enum]::GetNames([Security2.ApplyTo])) | Sort-Object) -join ',' | |
||||
|
Should -Be (($scopeNames | Sort-Object) -join ',') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Access rule scopes' { |
||||
|
It 'Should add and remove <Name> using <Form> on <Source>, preserving the other account' -ForEach $scopeCases { |
||||
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AccessScope' -Directory |
||||
|
$before = (Get-Acl -LiteralPath $folder).GetSecurityDescriptorSddlForm('Access') |
||||
|
$location = if ($Source -eq 'Path') { @{ Path = $folder } } else { @{ SecurityDescriptor = Get-NTFSSecurityDescriptor -Path $folder -ErrorAction Stop } } |
||||
|
$flags = @{ InheritanceFlags = $Inheritance; PropagationFlags = $Propagation } |
||||
|
$addScope = if ($Form -eq 'AppliesTo') { @{ AppliesTo = $Name } } else { $flags } |
||||
|
$removeScope = if ($Form -eq 'AppliesTo') { $flags } else { @{ AppliesTo = $Name } } |
||||
|
Add-NTFSAccess @location -Account $keeper -AccessRights Delete -AppliesTo ThisFolderOnly -ErrorAction Stop |
||||
|
|
||||
|
$added = @(Add-NTFSAccess @location @addScope -Account $account -AccessRights ReadData -PassThru -ErrorAction Stop | |
||||
|
Where-Object -FilterScript { $_.Account.Sid -eq $account }) |
||||
|
|
||||
|
$added | Should -HaveCount 1 |
||||
|
$added[0] | Should -BeOfType [Security2.FileSystemAccessRule2] |
||||
|
$added[0].InheritanceFlags | Should -Be ([Security.AccessControl.InheritanceFlags] $Inheritance) |
||||
|
$added[0].PropagationFlags | Should -Be ([Security.AccessControl.PropagationFlags] $Propagation) |
||||
|
$added[0].AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) | Should -BeTrue |
||||
|
[Security2.FileSystemSecurity2]::ConvertToApplyTo($added[0].InheritanceFlags, $added[0].PropagationFlags).ToString() | Should -Be $Name |
||||
|
if ($Source -eq 'SecurityDescriptor') { |
||||
|
(Get-Acl -LiteralPath $folder).GetSecurityDescriptorSddlForm('Access') | Should -BeExactly $before |
||||
|
} |
||||
|
|
||||
|
$remaining = @(Remove-NTFSAccess @location @removeScope -Account $account -AccessRights ReadData -RemoveSpecific -PassThru -ErrorAction Stop) |
||||
|
|
||||
|
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $account }) | Should -BeNullOrEmpty |
||||
|
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $keeper }) | Should -HaveCount 1 |
||||
|
@(Get-NTFSAccess @location -Account $account -ErrorAction Stop) | Should -BeNullOrEmpty |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Describe 'Access scopes on descendants' { |
||||
|
It 'Should apply <Name> only to its intended descendants, including the OneLevel boundary' -ForEach $propagationCases { |
||||
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Propagation' -Directory |
||||
|
Add-NTFSAccess -Path $folder -Account $account -AccessRights ReadData -AppliesTo $Name -ErrorAction Stop |
||||
|
$paths = [ordered]@{ |
||||
|
Root = $folder |
||||
|
File = Join-Path -Path $folder -ChildPath 'File.txt' |
||||
|
Child = Join-Path -Path $folder -ChildPath 'Child' |
||||
|
ChildFile = Join-Path -Path $folder -ChildPath 'Child\File.txt' |
||||
|
Grandchild = Join-Path -Path $folder -ChildPath 'Child\Grandchild' |
||||
|
GrandchildFile = Join-Path -Path $folder -ChildPath 'Child\Grandchild\File.txt' |
||||
|
} |
||||
|
Assert-TestSandboxPath -Sandbox $sandbox -Path @($paths.Values) |
||||
|
New-Item -ItemType Directory -Path $paths.Grandchild -Force | Out-Null |
||||
|
foreach ($key in 'File', 'ChildFile', 'GrandchildFile') { |
||||
|
Set-Content -LiteralPath $paths[$key] -Value $key |
||||
|
} |
||||
|
|
||||
|
$actual = @(foreach ($key in $paths.Keys) { |
||||
|
$active = @(Get-NTFSAccess -Path $paths[$key] -Account $account -ErrorAction Stop | Where-Object -FilterScript { |
||||
|
-not $_.PropagationFlags.HasFlag([Security.AccessControl.PropagationFlags]::InheritOnly) -and |
||||
|
$_.AccessRights.HasFlag([Security2.FileSystemRights2]::ReadData) |
||||
|
}) |
||||
|
if ($active.Count -gt 0) { $key } |
||||
|
}) |
||||
|
|
||||
|
($actual | Sort-Object) -join ',' | Should -Be (($ActiveTargets | Sort-Object) -join ',') |
||||
|
} |
||||
|
} |
||||
|
Describe 'Audit rule scopes' -Skip:(-not $canReadAudit) { |
||||
|
It 'Should add and remove <Name> using <Form> on <Source>, preserving the other account' -ForEach $scopeCases { |
||||
|
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'AuditScope' -Directory |
||||
|
$before = (Get-Acl -LiteralPath $folder -Audit).GetSecurityDescriptorSddlForm('Audit') |
||||
|
$location = if ($Source -eq 'Path') { @{ Path = $folder } } else { @{ SecurityDescriptor = Get-NTFSSecurityDescriptor -Path $folder -ErrorAction Stop } } |
||||
|
$flags = @{ InheritanceFlags = $Inheritance; PropagationFlags = $Propagation } |
||||
|
$addScope = if ($Form -eq 'AppliesTo') { @{ AppliesTo = $Name } } else { $flags } |
||||
|
$removeScope = if ($Form -eq 'AppliesTo') { $flags } else { @{ AppliesTo = $Name } } |
||||
|
Add-NTFSAudit @location -Account $keeper -AccessRights Delete -AuditFlags Failure -AppliesTo ThisFolderOnly -ErrorAction Stop |
||||
|
|
||||
|
$added = @(Add-NTFSAudit @location @addScope -Account $account -AccessRights ReadData -AuditFlags 'Success, Failure' -PassThru -ErrorAction Stop | |
||||
|
Where-Object -FilterScript { $_.Account.Sid -eq $account }) |
||||
|
|
||||
|
$added | Should -HaveCount 1 |
||||
|
$added[0] | Should -BeOfType [Security2.FileSystemAuditRule2] |
||||
|
$added[0].InheritanceFlags | Should -Be ([Security.AccessControl.InheritanceFlags] $Inheritance) |
||||
|
$added[0].PropagationFlags | Should -Be ([Security.AccessControl.PropagationFlags] $Propagation) |
||||
|
$added[0].AuditFlags | Should -Be ([Security.AccessControl.AuditFlags] 'Success, Failure') |
||||
|
[Security2.FileSystemSecurity2]::ConvertToApplyTo($added[0].InheritanceFlags, $added[0].PropagationFlags).ToString() | Should -Be $Name |
||||
|
if ($Source -eq 'SecurityDescriptor') { |
||||
|
(Get-Acl -LiteralPath $folder -Audit).GetSecurityDescriptorSddlForm('Audit') | Should -BeExactly $before |
||||
|
} |
||||
|
|
||||
|
$remaining = @(Remove-NTFSAudit @location @removeScope -Account $account -AccessRights ReadData -AuditFlags 'Success, Failure' -RemoveSpecific -PassThru -ErrorAction Stop) |
||||
|
|
||||
|
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $account }) | Should -BeNullOrEmpty |
||||
|
@($remaining | Where-Object -FilterScript { $_.Account.Sid -eq $keeper }) | Should -HaveCount 1 |
||||
|
@(Get-NTFSAudit @location -Account $account -ErrorAction Stop) | Should -BeNullOrEmpty |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,188 @@ |
|||||
|
<# |
||||
|
Tests Gallery publication recovery offline. Every network and publication command is mocked; the fake API key |
||||
|
exists only in the test process and is restored afterwards. Package hashes come from a sandbox file. |
||||
|
#> |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester shares variables between blocks.' |
||||
|
)] |
||||
|
param () |
||||
|
|
||||
|
BeforeAll { |
||||
|
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force |
||||
|
$sandbox = New-TestSandbox -Name 'PublishPackage' |
||||
|
$package = Join-Path -Path $sandbox -ChildPath 'NTFSSecurity.5.0.0-rc7.nupkg' |
||||
|
Assert-TestSandboxPath -Sandbox $sandbox -Path $package |
||||
|
[IO.File]::WriteAllBytes($package, [Text.Encoding]::UTF8.GetBytes('The package that CI built.')) |
||||
|
$sha512 = [Security.Cryptography.SHA512]::Create() |
||||
|
try { $hash = [Convert]::ToBase64String($sha512.ComputeHash([IO.File]::ReadAllBytes($package))) } |
||||
|
finally { $sha512.Dispose() } |
||||
|
$metadata = [pscustomobject]@{ entry = [pscustomobject]@{ properties = [pscustomobject]@{ |
||||
|
Id = 'NTFSSecurity'; Version = '5.0.0-rc7'; PackageHashAlgorithm = 'SHA512'; PackageHash = $hash |
||||
|
} } } |
||||
|
$published = [pscustomobject]@{ Name = 'NTFSSecurity'; Version = [version]'5.0.0'; Prerelease = 'rc7' } |
||||
|
$scriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\.github\scripts\Publish-ModulePackage.ps1' |
||||
|
$originalKey = $env:PSGALLERY_API_KEY |
||||
|
|
||||
|
# Stubs keep these tests available in Windows PowerShell, where PSResourceGet might not be installed. |
||||
|
function Find-PSResource { |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSReviewUnusedParameter', '', Justification = 'Command stub supplies parameter metadata for Pester mocks.' |
||||
|
)] |
||||
|
[CmdletBinding()] |
||||
|
param ([string] $Name, [string] $Version, [switch] $Prerelease, [string] $Repository) |
||||
|
throw 'Find-PSResource must be mocked in this test.' |
||||
|
} |
||||
|
function Publish-PSResource { |
||||
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( |
||||
|
'PSReviewUnusedParameter', '', Justification = 'Command stub supplies parameter metadata for Pester mocks.' |
||||
|
)] |
||||
|
[CmdletBinding()] |
||||
|
param ([string] $NupkgPath, [string] $Repository, [string] $ApiKey) |
||||
|
throw 'Publish-PSResource must be mocked in this test.' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
AfterAll { |
||||
|
$env:PSGALLERY_API_KEY = $originalKey |
||||
|
Remove-TestSandbox -Sandbox $sandbox |
||||
|
} |
||||
|
|
||||
|
Describe 'Publish-ModulePackage.ps1' { |
||||
|
BeforeEach { |
||||
|
$env:PSGALLERY_API_KEY = 'test-only-api-key' |
||||
|
Mock -CommandName Find-PSResource |
||||
|
Mock -CommandName Publish-PSResource |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { $metadata } |
||||
|
} |
||||
|
|
||||
|
It 'Should publish a new version using the environment key and the verified package path' { |
||||
|
& $scriptPath -NupkgPath $package -Version '5.0.0-rc7' |
||||
|
|
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 1 -Exactly -ParameterFilter { |
||||
|
$NupkgPath -eq $package -and $Repository -eq 'PSGallery' -and $ApiKey -eq 'test-only-api-key' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
It 'Should skip publication only after checking the existing package hash' { |
||||
|
Mock -CommandName Find-PSResource -MockWith { $published } |
||||
|
|
||||
|
& $scriptPath -NupkgPath $package -Version '5.0.0-rc7' |
||||
|
|
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
Should -Invoke -CommandName Invoke-RestMethod -Times 1 -Exactly -ParameterFilter { |
||||
|
$Uri -eq "https://www.powershellgallery.com/api/v2/Packages(Id='NTFSSecurity',Version='5.0.0-rc7')" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
It 'Should refuse an existing version containing a different package' { |
||||
|
Mock -CommandName Find-PSResource -MockWith { $published } |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { |
||||
|
[pscustomobject]@{ entry = [pscustomobject]@{ properties = [pscustomobject]@{ PackageHashAlgorithm = 'SHA512'; PackageHash = 'different-package' } } } |
||||
|
} |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*different package*' |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should refuse metadata without a usable SHA512 package hash: <Case>' -ForEach @( |
||||
|
@{ Case = 'missing hash'; Algorithm = 'SHA512'; PackageHash = '' } |
||||
|
@{ Case = 'wrong algorithm'; Algorithm = 'SHA256'; PackageHash = 'not-sha512' } |
||||
|
) { |
||||
|
Mock -CommandName Find-PSResource -MockWith { $published } |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { |
||||
|
[pscustomobject]@{ entry = [pscustomobject]@{ properties = [pscustomobject]@{ PackageHashAlgorithm = $Algorithm; PackageHash = $PackageHash } } } |
||||
|
} |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*SHA512*' |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should recover an uncertain upload only when the exact package appears in the Gallery' { |
||||
|
$script:lookups = 0 |
||||
|
Mock -CommandName Find-PSResource -MockWith { $script:lookups++; if ($script:lookups -gt 1) { $published } } |
||||
|
Mock -CommandName Publish-PSResource -MockWith { throw '409: a package with this version already exists.' } |
||||
|
|
||||
|
& $scriptPath -NupkgPath $package -Version '5.0.0-rc7' -WarningVariable uploadWarnings -WarningAction SilentlyContinue |
||||
|
|
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 1 -Exactly |
||||
|
Should -Invoke -CommandName Find-PSResource -Times 2 -Exactly |
||||
|
Should -Invoke -CommandName Invoke-RestMethod -Times 1 -Exactly |
||||
|
$uploadWarnings | Should -HaveCount 1 |
||||
|
$uploadWarnings[0].Message | Should -BeLike '*verified*exact package*' |
||||
|
} |
||||
|
|
||||
|
It 'Should preserve the upload error when the version remains absent' { |
||||
|
Mock -CommandName Publish-PSResource -MockWith { throw 'Upload failed: the server is unavailable.' } |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*Upload failed: the server is unavailable*' |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 1 -Exactly |
||||
|
Should -Invoke -CommandName Find-PSResource -Times 2 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should preserve the upload error when verification finds a different package' { |
||||
|
$script:lookups = 0 |
||||
|
Mock -CommandName Find-PSResource -MockWith { $script:lookups++; if ($script:lookups -gt 1) { $published } } |
||||
|
Mock -CommandName Publish-PSResource -MockWith { throw 'Upload failed: version collision.' } |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { |
||||
|
[pscustomobject]@{ entry = [pscustomobject]@{ properties = [pscustomobject]@{ PackageHashAlgorithm = 'SHA512'; PackageHash = 'different-package' } } } |
||||
|
} |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' -WarningAction SilentlyContinue } | Should -Throw -ExpectedMessage '*Upload failed: version collision*' |
||||
|
Should -Invoke -CommandName Invoke-RestMethod -Times 1 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should not publish after a lookup fails for a reason other than a missing version' { |
||||
|
Mock -CommandName Find-PSResource -MockWith { Write-Error -Message 'Lookup failed.' -ErrorId RepositoryUnavailable } |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*Lookup failed*' |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should compare Base64 hashes case-sensitively' { |
||||
|
Mock -CommandName Find-PSResource -MockWith { $published } |
||||
|
$differentCase = $hash.ToLowerInvariant() |
||||
|
($hash -ceq $differentCase) | Should -BeFalse |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { |
||||
|
[pscustomobject]@{ entry = [pscustomobject]@{ properties = [pscustomobject]@{ PackageHashAlgorithm = 'SHA512'; PackageHash = $differentCase } } } |
||||
|
} |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*different package*' |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should preserve the upload error when post-upload metadata is unavailable' { |
||||
|
$script:lookups = 0 |
||||
|
Mock -CommandName Find-PSResource -MockWith { $script:lookups++; if ($script:lookups -gt 1) { $published } } |
||||
|
Mock -CommandName Publish-PSResource -MockWith { throw 'Upload failed: original error.' } |
||||
|
Mock -CommandName Invoke-RestMethod -MockWith { throw 'Metadata is unavailable.' } |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' -WarningAction SilentlyContinue } | Should -Throw -ExpectedMessage '*Upload failed: original error*' |
||||
|
Should -Invoke -CommandName Invoke-RestMethod -Times 1 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should preserve the upload error when the post-upload lookup fails' { |
||||
|
$script:lookups = 0 |
||||
|
Mock -CommandName Find-PSResource -MockWith { |
||||
|
$script:lookups++ |
||||
|
if ($script:lookups -gt 1) { Write-Error -Message 'Post-upload lookup failed.' -ErrorId RepositoryUnavailable } |
||||
|
} |
||||
|
Mock -CommandName Publish-PSResource -MockWith { throw 'Upload failed: original error.' } |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' -WarningAction SilentlyContinue } | Should -Throw -ExpectedMessage '*Upload failed: original error*' |
||||
|
Should -Invoke -CommandName Find-PSResource -Times 2 -Exactly |
||||
|
} |
||||
|
It 'Should allow the expected PackageNotFound probe result before publishing' { |
||||
|
Mock -CommandName Find-PSResource -MockWith { Write-Error -Message 'Not published yet.' -ErrorId PackageNotFound } |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Not -Throw |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 1 -Exactly |
||||
|
} |
||||
|
|
||||
|
It 'Should reject a missing API key before contacting the Gallery' { |
||||
|
$env:PSGALLERY_API_KEY = $null |
||||
|
|
||||
|
{ & $scriptPath -NupkgPath $package -Version '5.0.0-rc7' } | Should -Throw -ExpectedMessage '*PSGALLERY_API_KEY*not set*' |
||||
|
Should -Invoke -CommandName Find-PSResource -Times 0 -Exactly |
||||
|
Should -Invoke -CommandName Publish-PSResource -Times 0 -Exactly |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue