mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-11-01 07:53:42 +08:00
fix(mxc): harden provider credential test runner
- preserve native JSON arguments under Windows PowerShell - tolerate existing gateway registrations - reject network executable paths with actionable launch diagnostics - record redacted failure context in result bundles Signed-off-by: Akber Raza <akberr@nvidia.com>
This commit is contained in:
@@ -12,9 +12,12 @@
|
|||||||
# $env:GITHUB_TOKEN = "github_pat_..."
|
# $env:GITHUB_TOKEN = "github_pat_..."
|
||||||
# mise run --skip-tools windows:build:x64
|
# mise run --skip-tools windows:build:x64
|
||||||
#
|
#
|
||||||
# Run from a demo-package folder containing openshell-gateway.exe, openshell.exe,
|
# Run from a local demo-package folder containing openshell-gateway.exe,
|
||||||
# the PowerShell probe, and the three configuration fixtures beside this script,
|
# openshell.exe, the PowerShell probe, and the three configuration fixtures
|
||||||
# or pass explicit gateway and CLI paths:
|
# beside this script, or pass explicit local gateway and CLI paths. When the
|
||||||
|
# script is copied to a network share, keep the executables on a local volume;
|
||||||
|
# Windows Application Control commonly rejects unsigned development binaries
|
||||||
|
# launched from UNC or mapped network paths.
|
||||||
#
|
#
|
||||||
# powershell -NoProfile -ExecutionPolicy Bypass `
|
# powershell -NoProfile -ExecutionPolicy Bypass `
|
||||||
# -File .\run-provider-credential-test.ps1 `
|
# -File .\run-provider-credential-test.ps1 `
|
||||||
@@ -45,7 +48,10 @@ $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|||||||
$resultDir = Join-Path $here "results-provider-credential-$stamp"
|
$resultDir = Join-Path $here "results-provider-credential-$stamp"
|
||||||
New-Item -ItemType Directory -Force $resultDir | Out-Null
|
New-Item -ItemType Directory -Force $resultDir | Out-Null
|
||||||
|
|
||||||
function Step([string]$message) { Write-Host "`n=== $message ===" -ForegroundColor Cyan }
|
function Step([string]$message) {
|
||||||
|
$script:failureStage = $message
|
||||||
|
Write-Host "`n=== $message ===" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
function Info([string]$message) { Write-Host " $message" }
|
function Info([string]$message) { Write-Host " $message" }
|
||||||
function Ok([string]$message) { Write-Host "[OK] $message" -ForegroundColor Green }
|
function Ok([string]$message) { Write-Host "[OK] $message" -ForegroundColor Green }
|
||||||
function Bad([string]$message) { Write-Host "[FAIL] $message" -ForegroundColor Red }
|
function Bad([string]$message) { Write-Host "[FAIL] $message" -ForegroundColor Red }
|
||||||
@@ -59,25 +65,95 @@ function Resolve-Artifact([string]$explicit, [string]$leaf) {
|
|||||||
|
|
||||||
function Escape-Toml([string]$value) { return $value.Replace('\', '\\') }
|
function Escape-Toml([string]$value) { return $value.Replace('\', '\\') }
|
||||||
|
|
||||||
function Invoke-Cli([string[]]$CommandArgs, [switch]$AllowFailure) {
|
function Test-NetworkPath([string]$value) {
|
||||||
$previous = $ErrorActionPreference
|
if ($value.StartsWith('\\')) { return $true }
|
||||||
$ErrorActionPreference = "Continue"
|
if ($value -notmatch '^([A-Za-z]):[\\/]') { return $false }
|
||||||
try {
|
|
||||||
$lines = & $cli @CommandArgs 2>&1
|
$drive = Get-PSDrive -Name $Matches[1] -PSProvider FileSystem -ErrorAction SilentlyContinue
|
||||||
$exitCode = $LASTEXITCODE
|
return $null -ne $drive -and
|
||||||
} finally {
|
-not [string]::IsNullOrWhiteSpace($drive.DisplayRoot) -and
|
||||||
$ErrorActionPreference = $previous
|
$drive.DisplayRoot.StartsWith('\\')
|
||||||
}
|
}
|
||||||
# Windows PowerShell wraps native stderr lines in ErrorRecord objects and
|
|
||||||
# Out-String adds a misleading "At ... NativeCommandError" block even when
|
function Assert-LocalExecutable([string]$label, [string]$path) {
|
||||||
# the command succeeds. Preserve the message without that decoration.
|
if (-not (Test-NetworkPath $path)) { return }
|
||||||
$text = (($lines | ForEach-Object {
|
|
||||||
if ($_ -is [System.Management.Automation.ErrorRecord]) {
|
throw "$label executable resolves to network path '$path'. Windows Application Control can block unsigned development binaries launched from network locations. Pass -GatewayPath and -CliPath pointing to local build outputs (for example, the repository's target\x86_64-pc-windows-msvc\release directory); the script and result artifacts may remain on the network share."
|
||||||
$_.Exception.Message
|
}
|
||||||
} else {
|
|
||||||
$_.ToString()
|
function Get-LaunchFailureMessage([string]$label, [string]$path, [System.Exception]$exception) {
|
||||||
|
$messages = New-Object System.Collections.Generic.List[string]
|
||||||
|
$currentException = $exception
|
||||||
|
while ($null -ne $currentException) {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($currentException.Message)) {
|
||||||
|
[void]$messages.Add($currentException.Message)
|
||||||
}
|
}
|
||||||
}) -join [Environment]::NewLine).Trim()
|
$currentException = $currentException.InnerException
|
||||||
|
}
|
||||||
|
$message = ($messages -join ' | ')
|
||||||
|
|
||||||
|
if ($message -match '(?i)Application Control policy has blocked this file') {
|
||||||
|
$sha256 = try { (Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash } catch { "unavailable" }
|
||||||
|
$signature = try { (Get-AuthenticodeSignature -LiteralPath $path -ErrorAction Stop).Status } catch { "unavailable" }
|
||||||
|
return "Application Control blocked $label launch '$path' (SHA256=$sha256; Authenticode=$signature). Use a local, policy-approved binary and review the applicable App Control event log if the local launch is also blocked. Original error: $message"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "failed to launch $label '$path': $message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build one CreateProcess-compatible command-line argument. Windows PowerShell
|
||||||
|
# 5.1 removes embedded quotes from JSON passed to native commands through the
|
||||||
|
# call operator, which corrupts --driver-config-json before the CLI parses it.
|
||||||
|
function Quote-NativeArgument([string]$value) {
|
||||||
|
if ($value.Length -gt 0 -and $value -notmatch '[\s"]') { return $value }
|
||||||
|
|
||||||
|
$quoted = New-Object System.Text.StringBuilder
|
||||||
|
[void]$quoted.Append('"')
|
||||||
|
$backslashes = 0
|
||||||
|
foreach ($ch in $value.ToCharArray()) {
|
||||||
|
if ($ch -eq '\') {
|
||||||
|
$backslashes++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($ch -eq '"') {
|
||||||
|
[void]$quoted.Append(('\' * (2 * $backslashes + 1)))
|
||||||
|
[void]$quoted.Append('"')
|
||||||
|
} else {
|
||||||
|
if ($backslashes -gt 0) { [void]$quoted.Append(('\' * $backslashes)) }
|
||||||
|
[void]$quoted.Append($ch)
|
||||||
|
}
|
||||||
|
$backslashes = 0
|
||||||
|
}
|
||||||
|
if ($backslashes -gt 0) { [void]$quoted.Append(('\' * (2 * $backslashes))) }
|
||||||
|
[void]$quoted.Append('"')
|
||||||
|
return $quoted.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Cli([string[]]$CommandArgs, [switch]$AllowFailure) {
|
||||||
|
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$startInfo.FileName = $cli
|
||||||
|
$startInfo.Arguments = (($CommandArgs | ForEach-Object { Quote-NativeArgument $_ }) -join ' ')
|
||||||
|
$startInfo.UseShellExecute = $false
|
||||||
|
$startInfo.CreateNoWindow = $true
|
||||||
|
$startInfo.RedirectStandardOutput = $true
|
||||||
|
$startInfo.RedirectStandardError = $true
|
||||||
|
|
||||||
|
$process = New-Object System.Diagnostics.Process
|
||||||
|
$process.StartInfo = $startInfo
|
||||||
|
try {
|
||||||
|
if (-not $process.Start()) { throw "failed to start $cli" }
|
||||||
|
} catch {
|
||||||
|
throw (Get-LaunchFailureMessage "CLI" $cli $_.Exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
$stdout = $process.StandardOutput.ReadToEndAsync()
|
||||||
|
$stderr = $process.StandardError.ReadToEndAsync()
|
||||||
|
$process.WaitForExit()
|
||||||
|
$exitCode = $process.ExitCode
|
||||||
|
$text = (@($stdout.Result, $stderr.Result) | Where-Object {
|
||||||
|
-not [string]::IsNullOrWhiteSpace($_)
|
||||||
|
}) -join [Environment]::NewLine
|
||||||
|
$text = $text.Trim()
|
||||||
if (-not $AllowFailure -and $exitCode -ne 0) {
|
if (-not $AllowFailure -and $exitCode -ne 0) {
|
||||||
throw "openshell $($CommandArgs -join ' ') failed (exit $exitCode): $text"
|
throw "openshell $($CommandArgs -join ' ') failed (exit $exitCode): $text"
|
||||||
}
|
}
|
||||||
@@ -133,6 +209,8 @@ $providerName = "mxc-github-e2e"
|
|||||||
$passed = $false
|
$passed = $false
|
||||||
$rawTokenLeak = $false
|
$rawTokenLeak = $false
|
||||||
$artifactScanFailed = $false
|
$artifactScanFailed = $false
|
||||||
|
$failureReason = ""
|
||||||
|
$failureStage = "initialization"
|
||||||
$githubToken = $env:GITHUB_TOKEN
|
$githubToken = $env:GITHUB_TOKEN
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -144,6 +222,8 @@ try {
|
|||||||
if (-not (Test-Path $file)) { throw "missing artifact: $file" }
|
if (-not (Test-Path $file)) { throw "missing artifact: $file" }
|
||||||
Info "found $file"
|
Info "found $file"
|
||||||
}
|
}
|
||||||
|
Assert-LocalExecutable "gateway" $gateway
|
||||||
|
Assert-LocalExecutable "CLI" $cli
|
||||||
if (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue) {
|
if (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue) {
|
||||||
throw "gateway port $Port is already in use"
|
throw "gateway port $Port is already in use"
|
||||||
}
|
}
|
||||||
@@ -222,10 +302,14 @@ try {
|
|||||||
# a successful test cannot be attributed to gateway environment inheritance.
|
# a successful test cannot be attributed to gateway environment inheritance.
|
||||||
Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
|
Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
|
||||||
try {
|
try {
|
||||||
$gw = Start-Process -FilePath $gateway `
|
try {
|
||||||
-ArgumentList @("--disable-tls", "--db-url", "sqlite::memory:", "--log-level", "info") `
|
$gw = Start-Process -FilePath $gateway `
|
||||||
-WorkingDirectory $here -PassThru -NoNewWindow `
|
-ArgumentList @("--disable-tls", "--db-url", "sqlite::memory:", "--log-level", "info") `
|
||||||
-RedirectStandardOutput $gwLog -RedirectStandardError $gwErrLog
|
-WorkingDirectory $here -PassThru -NoNewWindow `
|
||||||
|
-RedirectStandardOutput $gwLog -RedirectStandardError $gwErrLog
|
||||||
|
} catch {
|
||||||
|
throw (Get-LaunchFailureMessage "gateway" $gateway $_.Exception)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$env:GITHUB_TOKEN = $githubToken
|
$env:GITHUB_TOKEN = $githubToken
|
||||||
}
|
}
|
||||||
@@ -242,7 +326,12 @@ try {
|
|||||||
|
|
||||||
Step "Configure provider and effective policy"
|
Step "Configure provider and effective policy"
|
||||||
$env:OPENSHELL_GATEWAY = ""
|
$env:OPENSHELL_GATEWAY = ""
|
||||||
Invoke-Cli @("gateway", "add", "http://127.0.0.1:$Port", "--local", "--name", $GatewayName) | Out-Null
|
$gatewayAdd = Invoke-Cli @(
|
||||||
|
"gateway", "add", "http://127.0.0.1:$Port", "--local", "--name", $GatewayName
|
||||||
|
) -AllowFailure
|
||||||
|
if ($gatewayAdd.ExitCode -ne 0 -and $gatewayAdd.Text -notmatch '(?i)already exists') {
|
||||||
|
throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)"
|
||||||
|
}
|
||||||
Invoke-Cli @("gateway", "select", $GatewayName) | Out-Null
|
Invoke-Cli @("gateway", "select", $GatewayName) | Out-Null
|
||||||
Invoke-Cli @("provider", "profile", "lint", "--file", $profileUsed) | Out-Null
|
Invoke-Cli @("provider", "profile", "lint", "--file", $profileUsed) | Out-Null
|
||||||
Invoke-Cli @("provider", "profile", "import", "--file", $profileUsed) | Out-Null
|
Invoke-Cli @("provider", "profile", "import", "--file", $profileUsed) | Out-Null
|
||||||
@@ -294,7 +383,11 @@ try {
|
|||||||
Ok "placeholder isolation, authorized rewrite, and endpoint mismatch all passed"
|
Ok "placeholder isolation, authorized rewrite, and endpoint mismatch all passed"
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Bad $_.Exception.Message
|
$failureReason = ($_.Exception.Message -replace '\r?\n', ' | ').Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($githubToken)) {
|
||||||
|
$failureReason = $failureReason.Replace($githubToken, "***REDACTED***")
|
||||||
|
}
|
||||||
|
Bad $failureReason
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if ($cli -and $sandboxName -and $gw -and -not $gw.HasExited) {
|
if ($cli -and $sandboxName -and $gw -and -not $gw.HasExited) {
|
||||||
@@ -342,6 +435,8 @@ sandbox : $sandboxName
|
|||||||
backend : process_container
|
backend : process_container
|
||||||
provider : $providerName
|
provider : $providerName
|
||||||
share_path : $ShareDir
|
share_path : $ShareDir
|
||||||
|
stage : $failureStage
|
||||||
|
failure : $(if ([string]::IsNullOrWhiteSpace($failureReason)) { "none" } else { $failureReason })
|
||||||
|
|
||||||
PASS proves:
|
PASS proves:
|
||||||
- MXC received a revision-scoped GITHUB_TOKEN placeholder, not the token.
|
- MXC received a revision-scoped GITHUB_TOKEN placeholder, not the token.
|
||||||
|
|||||||
Reference in New Issue
Block a user