mirror of https://github.com/raandree/NTFSSecurity
Browse Source
The independent review of the acceptance record found no Blocker or Major issue. This commit corrects what it found: the commit that fixed the break row, what the State test shows, the baseline failures that carry no message, the count of results, the wording about the folders before the first run, the truncated messages in the results file, the README row of case 10, and the review section of the record. The 42 messageless baseline failures are now explained by a diagnostic that runs the bodies of those tests in a TEMP sandbox: on the base, the second item is removed, copied, moved, re-owned, or rewritten after Select-Object -First 1 or a throw; on the candidate it stays. The diagnostic, the check of the result files, and a read-only check of a published version are in Tests/Lab/Acceptance. Decision 22 no longer says that Copy-Item2 is like Copy-Item for a folder: the built-in Copy-Item creates the missing parent folders of a folder copy, Copy-Item2 of rc6 and of the candidate doesn't. The choice stays, the reference point is corrected, and the question is left to the maintainer. The migration hint of item 8 is stated as it is. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant <ai@example.com>pull/119/head
7 changed files with 538 additions and 46 deletions
|
@ -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 |
|||
} |
|||
@ -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' |
|||
@ -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 |
|||
Loading…
Reference in new issue