diff --git a/.azure-pipelines/hidi-release.yml b/.azure-pipelines/hidi-release.yml
index 0087ca80..c84fefd3 100644
--- a/.azure-pipelines/hidi-release.yml
+++ b/.azure-pipelines/hidi-release.yml
@@ -165,11 +165,12 @@ extends:
MaxRetryAttempts: '5'
PendingAnalysisWaitTimeoutMinutes: '5'
- pwsh: |
- Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll
+ $ErrorActionPreference = 'Stop'
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.exe artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe
New-Item -ItemType Directory -Force '$(Build.ArtifactStagingDirectory)\hidi' | Out-Null
- dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg -o '$(Build.ArtifactStagingDirectory)\hidi'
+ dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg "/p:HidiSignedAssemblyPath=$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll" -o '$(Build.ArtifactStagingDirectory)\hidi'
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & .\scripts\verify-hidi-package-assembly.ps1 -PackagePath '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.$(HidiVersion).nupkg' -SignedAssemblyPath '$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll'
Compress-Archive -Path artifacts\hidi\win-x64\* -DestinationPath '$(Build.ArtifactStagingDirectory)\hidi\hidi-win-x64-$(HidiVersion).zip'
Copy-Item artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.exe'
displayName: Pack signed hidi binaries
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index afa7c5c2..3fc94750 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -26,10 +26,12 @@ jobs:
- name: Data gatherer
id: data_gatherer
shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
run: |
# Get default branch
$repo = 'microsoft/OpenAPI.NET.OData'
- $defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo | Select-Object -ExpandProperty default_branch
+ $defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo -Headers @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | Select-Object -ExpandProperty default_branch
Write-Output "default_branch=$(echo $defaultBranch) >> $GITHUB_OUTPUT"
- name: Conditionals handler
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index d717f338..3681312c 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -60,59 +60,65 @@ jobs:
- name: Install PowerShell test dependency
shell: pwsh
run: Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
- - name: Test Hidi NuGet helper with measured coverage
+ - name: Test Hidi NuGet helpers with measured coverage
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop
- $helperPath = 'scripts/check-nuget-package-published.ps1'
- $helper = (Resolve-Path $helperPath).Path
+ $helperPaths = @('scripts/check-nuget-package-published.ps1', 'scripts/verify-hidi-package-assembly.ps1')
+ $helpers = @($helperPaths | ForEach-Object { (Resolve-Path $_).Path })
$outputRoot = Join-Path $PWD 'artifacts\hidi\powershell-coverage'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
$configuration = New-PesterConfiguration
- $configuration.Run.Path = 'test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1'
+ $configuration.Run.Path = @('test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1', 'test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1')
$configuration.Run.PassThru = $true
$configuration.CodeCoverage.Enabled = $true
- $configuration.CodeCoverage.Path = $helper
+ $configuration.CodeCoverage.Path = $helpers
$configuration.CodeCoverage.OutputFormat = 'JaCoCo'
$configuration.CodeCoverage.OutputPath = Join-Path $outputRoot 'pester-coverage.xml'
$configuration.CodeCoverage.CoveragePercentTarget = 80
$result = Invoke-Pester -Configuration $configuration
- if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 24) {
- throw "Hidi NuGet helper tests failed or did not execute all 24 cases."
+ if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 45) {
+ throw "Hidi NuGet helper tests failed or did not execute all 45 cases."
}
$report = [xml](Get-Content $configuration.CodeCoverage.OutputPath.Value -Raw)
$sourceFiles = @($report.SelectNodes('//sourcefile'))
- if ($sourceFiles.Count -ne 1 -or $sourceFiles[0].name -ne [IO.Path]::GetFileName($helper)) {
- throw 'Expected measured coverage of only the Hidi NuGet helper.'
- }
- $lines = @($sourceFiles[0].SelectNodes('line'))
- $sourceLineCount = @(Get-Content $helper).Count
- $lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
- if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
- @($lines | Where-Object {
- [int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
- -not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
- [int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
- }).Count -gt 0) {
- throw 'Missing or invalid measured helper coverage lines.'
- }
- $covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
- if ($covered / $lines.Count -lt 0.8) {
- throw "Insufficient measured Hidi helper coverage: $covered/$($lines.Count) lines."
+ if ($sourceFiles.Count -ne $helpers.Count) {
+ throw 'Expected measured coverage of both Hidi NuGet helpers.'
}
$coverage = [xml]''
- $file = $coverage.CreateElement('file')
- $file.SetAttribute('path', $helperPath)
- [void]$coverage.DocumentElement.AppendChild($file)
- foreach ($line in $lines) {
- $entry = $coverage.CreateElement('lineToCover')
- $entry.SetAttribute('lineNumber', $line.nr)
- $entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
- [void]$file.AppendChild($entry)
+ foreach ($helperPath in $helperPaths) {
+ $helper = (Resolve-Path $helperPath).Path
+ $source = @($sourceFiles | Where-Object { $_.name -eq [IO.Path]::GetFileName($helper) })
+ if ($source.Count -ne 1) { throw "Missing or ambiguous measured coverage for $helperPath." }
+ $lines = @($source[0].SelectNodes('line'))
+ $sourceLineCount = @(Get-Content $helper).Count
+ $lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
+ if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
+ @($lines | Where-Object {
+ [int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
+ -not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
+ [int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
+ }).Count -gt 0) {
+ throw "Missing or invalid measured helper coverage lines for $helperPath."
+ }
+ $covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
+ if ($covered / $lines.Count -lt 0.8) {
+ throw "Insufficient measured Hidi helper coverage for ${helperPath}: $covered/$($lines.Count) lines."
+ }
+ $file = $coverage.CreateElement('file')
+ $file.SetAttribute('path', $helperPath)
+ [void]$coverage.DocumentElement.AppendChild($file)
+ foreach ($line in $lines) {
+ $entry = $coverage.CreateElement('lineToCover')
+ $entry.SetAttribute('lineNumber', $line.nr)
+ $entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
+ [void]$file.AppendChild($entry)
+ }
+ Write-Host "Measured Hidi NuGet helper coverage for ${helperPath}: $covered/$($lines.Count) lines."
}
$coverage.Save((Join-Path $outputRoot 'sonar-coverage.xml'))
- Write-Host "Measured Hidi NuGet helper coverage: $covered/$($lines.Count) lines; Sonar generic report generated."
+ Write-Host "Sonar generic report generated from measured coverage."
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
diff --git a/README.md b/README.md
index a73ea9b1..c8fd6df7 100644
--- a/README.md
+++ b/README.md
@@ -116,12 +116,12 @@ feed responses fail closed. Releases must use an exact `hidi-v2.` tag
matching the project version and be newer than the `2.12.2` migration baseline.
This pipeline implementation does not enable publishing or authorize resources.
-The SonarCloud workflow runs the private-feed helper tests with Pester 5.7.1 and
-converts measured JaCoCo line hits to Sonar generic coverage, alongside the
-existing C# OpenCover reports. The Pester script resides inside the Hidi test
+The SonarCloud workflow runs the private-feed and signed-payload helper tests
+with Pester 5.7.1 and converts measured JaCoCo line hits to Sonar generic coverage,
+alongside existing C# OpenCover reports. The Pester scripts reside inside the Hidi test
project directory so Sonar assigns it to test sources; a linked sibling file
does not establish that ownership. Run the helper tests locally with
-`Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester .\test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1`.
+`Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester .\test\Microsoft.OpenApi.Hidi.Tests\*.Tests.ps1`.
---
diff --git a/scripts/verify-hidi-package-assembly.ps1 b/scripts/verify-hidi-package-assembly.ps1
new file mode 100644
index 00000000..53365fe7
--- /dev/null
+++ b/scripts/verify-hidi-package-assembly.ps1
@@ -0,0 +1,59 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+<#
+.SYNOPSIS
+Verifies the Hidi tool package contains the exact Microsoft-signed staging DLL.
+.DESCRIPTION
+Run after tool packing and before NuGet signing. A signed NuGet container does
+not prove that its assembly payload retained the ESRP signature.
+#>
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$PackagePath,
+ [Parameter(Mandatory = $true)]
+ [string]$SignedAssemblyPath
+)
+
+$ErrorActionPreference = 'Stop'
+
+foreach ($path in @($PackagePath, $SignedAssemblyPath)) {
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
+ throw "Missing Hidi signing verification input: $path"
+ }
+}
+
+$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString())
+New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
+$packagedAssembly = Join-Path $temporaryDirectory 'Microsoft.OpenApi.Hidi.dll'
+$archive = $null
+try {
+ $archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path)
+ $assemblies = @($archive.Entries | Where-Object { $_.Name -ieq 'Microsoft.OpenApi.Hidi.dll' })
+ if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') {
+ throw 'Expected exactly one Hidi DLL at tools/net8.0/any/Microsoft.OpenApi.Hidi.dll.'
+ }
+ [IO.Compression.ZipFileExtensions]::ExtractToFile($assemblies[0], $packagedAssembly)
+
+ $stagingHash = (Get-FileHash -LiteralPath $SignedAssemblyPath -Algorithm SHA256).Hash
+ $packageHash = (Get-FileHash -LiteralPath $packagedAssembly -Algorithm SHA256).Hash
+ if ($packageHash -cne $stagingHash) {
+ throw "Packaged Hidi DLL differs from the signed staging DLL: $packageHash != $stagingHash"
+ }
+ foreach ($assembly in @($SignedAssemblyPath, $packagedAssembly)) {
+ $signature = Get-AuthenticodeSignature -LiteralPath $assembly
+ if ($signature.Status -ne 'Valid' -or
+ $signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
+ throw "Hidi DLL must have a valid Microsoft Corporation Authenticode signature: $assembly ($($signature.Status))"
+ }
+ }
+ Write-Host "Verified packaged Hidi DLL: valid Microsoft Authenticode signature; signed staging SHA256=$stagingHash"
+}
+finally {
+ if ($null -ne $archive) { $archive.Dispose() }
+ if (Test-Path -LiteralPath $packagedAssembly) {
+ Remove-Item -LiteralPath $packagedAssembly -Force
+ }
+ Remove-Item -LiteralPath $temporaryDirectory -Force
+}
diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
index b1cf4efa..ad64f08b 100644
--- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
+++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
@@ -68,4 +68,25 @@
+
+
+
+
+ <_HidiAssemblyToReplace Include="@(ResolvedFileToPublish)"
+ Condition="'%(ResolvedFileToPublish.RelativePath)' == '$(TargetFileName)'"/>
+
+
+
+
+
+ $(TargetFileName)
+ Always
+
+
+
+
diff --git a/src/Microsoft.OpenApi.Hidi/readme.md b/src/Microsoft.OpenApi.Hidi/readme.md
index a922a5ee..56282a8b 100644
--- a/src/Microsoft.OpenApi.Hidi/readme.md
+++ b/src/Microsoft.OpenApi.Hidi/readme.md
@@ -72,6 +72,21 @@ Local/CI builds use the public-only strong-name identity. Official release
artifacts are signed in Azure Pipelines; private signing keys do not belong in
this repository. The migration baseline 2.12.2 is already published. Destination
NuGet, executable, and Docker publishing are disabled until source cutover.
+Official tool packing passes `HidiSignedAssemblyPath` to replace the Hidi DLL
+in the SDK's `ResolvedFileToPublish` items after `ComputeFilesToPublish`.
+`PackAsTool` publishes the intermediate assembly from `obj`, so replacing only
+the DLL in `bin` does not preserve its Authenticode signature. The opt-in target
+uses the exact ESRP staging DLL without recompiling it; ordinary local packing
+and Windows single-file publishing retain their default inputs.
+Before NuGet signing, `scripts/verify-hidi-package-assembly.ps1` requires exactly
+one DLL at `tools/net8.0/any/Microsoft.OpenApi.Hidi.dll`, verifies its SHA256
+matches the staging DLL, and requires valid Microsoft Corporation Authenticode
+signatures on both. Missing, mismatched, unsigned, or unverifiable payloads fail
+the build. A signed NuGet container alone is not assembly-signature evidence.
+Local byte-provenance checks and mocked signature unit tests cannot establish
+Microsoft ESRP signing readiness; that requires a publish-disabled official run
+after the normally approved merge.
+
The standard Release Please config tracks Hidi as its own component, with a
separate manifest version, project version, changelog, and `hidi-v2.*` tags.
The first destination package release must advance beyond the baseline and use
diff --git a/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1
new file mode 100644
index 00000000..4dd2a0e7
--- /dev/null
+++ b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1
@@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+BeforeDiscovery {
+ $cases = @(
+ @{ Name = 'matching signed DLL before NuGet signing' }
+ @{ Name = 'different payload bytes'; Payload = 'unsigned-build'; ExpectedError = '*differs from the signed staging DLL*' }
+ @{ Name = 'missing tool DLL'; Entries = @(); ExpectedError = '*exactly one Hidi DLL*' }
+ @{ Name = 'wrong tool location'; Entries = @('lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' }
+ @{ Name = 'duplicate tool DLL'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' }
+ @{ Name = 'extra tool DLL elsewhere'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' }
+ @{ Name = 'wrong entry casing'; Entries = @('tools/net8.0/any/microsoft.openapi.hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' }
+ @{ Name = 'unsigned package payload'; PackageStatus = 'NotSigned'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'damaged package signature'; PackageStatus = 'HashMismatch'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'unverifiable package signature'; PackageStatus = 'UnknownError'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'unsigned staging DLL'; StagingStatus = 'NotSigned'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'damaged staging signature'; StagingStatus = 'HashMismatch'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'non-Microsoft payload signer'; PackageSubject = 'CN=Example, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'non-Microsoft staging signer'; StagingSubject = 'CN=Example, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'misleading Microsoft signer name'; PackageSubject = 'CN=Microsoft Corporation, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'missing payload certificate'; MissingPackageCertificate = $true; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'missing staging certificate'; MissingStagingCertificate = $true; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' }
+ @{ Name = 'missing package'; MissingPackage = $true; ExpectedError = '*Missing Hidi signing verification input*' }
+ @{ Name = 'missing staging DLL'; MissingStaging = $true; ExpectedError = '*Missing Hidi signing verification input*' }
+ @{ Name = 'invalid package archive'; InvalidArchive = $true; ExpectedError = '*' }
+ @{ Name = 'signature verification error'; SignatureError = $true; ExpectedError = '*Signature verification unavailable*' }
+ )
+}
+
+Describe 'Hidi package signed-assembly provenance' {
+ BeforeAll {
+ $helper = Join-Path $PSScriptRoot '..\..\scripts\verify-hidi-package-assembly.ps1'
+ }
+
+ It '' -ForEach $cases {
+ $testCase = $_
+ $directory = Join-Path $TestDrive ([guid]::NewGuid().ToString())
+ New-Item -ItemType Directory -Path $directory | Out-Null
+ $package = Join-Path $directory 'Microsoft.OpenApi.Hidi.2.13.0.nupkg'
+ $staging = Join-Path $directory 'Microsoft.OpenApi.Hidi.dll'
+ if (-not $MissingStaging) { [IO.File]::WriteAllText($staging, 'signed-staging') }
+ if (-not $MissingPackage) {
+ if ($InvalidArchive) {
+ [IO.File]::WriteAllText($package, 'not-a-zip')
+ }
+ else {
+ $archive = [IO.Compression.ZipFile]::Open($package, 'Create')
+ try {
+ $entryNames = if ($testCase.ContainsKey('Entries')) { $Entries } else { @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') }
+ foreach ($name in $entryNames) {
+ $writer = [IO.StreamWriter]::new($archive.CreateEntry($name).Open())
+ try { $writer.Write($(if ($Payload) { $Payload } else { 'signed-staging' })) }
+ finally { $writer.Dispose() }
+ }
+ }
+ finally { $archive.Dispose() }
+ }
+ }
+ $inspectedPaths = [Collections.Generic.List[string]]::new()
+ Mock Get-AuthenticodeSignature {
+ param($LiteralPath)
+ $inspectedPaths.Add($LiteralPath)
+ if ($SignatureError) { throw 'Signature verification unavailable' }
+ $isStaging = $LiteralPath -eq $staging
+ $status = if ($isStaging -and $StagingStatus) { $StagingStatus }
+ elseif (-not $isStaging -and $PackageStatus) { $PackageStatus }
+ else { 'Valid' }
+ $subject = if ($isStaging -and $StagingSubject) { $StagingSubject }
+ elseif (-not $isStaging -and $PackageSubject) { $PackageSubject }
+ else { 'CN=Microsoft Corporation, O=Microsoft Corporation, C=US' }
+ $certificate = if (($isStaging -and $MissingStagingCertificate) -or
+ (-not $isStaging -and $MissingPackageCertificate)) { $null }
+ else { [pscustomobject]@{ Subject = $subject } }
+ [pscustomobject]@{ Status = $status; SignerCertificate = $certificate }
+ }
+
+ if ($ExpectedError) {
+ { & $helper -PackagePath $package -SignedAssemblyPath $staging } | Should -Throw $ExpectedError
+ }
+ else {
+ $output = & $helper -PackagePath $package -SignedAssemblyPath $staging 6>&1
+ $output | Should -Match "signed staging SHA256=$((Get-FileHash -LiteralPath $staging).Hash)"
+ Should -Invoke Get-AuthenticodeSignature -Times 2 -Exactly
+ }
+ foreach ($path in $inspectedPaths | Where-Object { $_ -ne $staging }) {
+ Test-Path -LiteralPath $path | Should -BeFalse
+ Test-Path -LiteralPath (Split-Path $path -Parent) | Should -BeFalse
+ }
+ }
+}