mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-26 06:52:57 +08:00
test(mxc): add workload-to-host service e2e coverage
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
@@ -452,7 +452,19 @@ variable by default). Require both a nonempty response and the OpenShell proxy
|
||||
CA issuer; never replace these assertions with an insecure TLS request. This
|
||||
fixture permits unavailable CRL information using best-effort Schannel revocation
|
||||
for the ephemeral private CA; this is not revocation qualification. This
|
||||
scenario is not part of the default six offline-capable E2E scenarios.
|
||||
scenario is not part of the default seven offline-capable E2E scenarios.
|
||||
|
||||
The default `host-service` scenario runs the HTTP fixture on an active host LAN
|
||||
IPv4 address and a dynamic port, then checks two nonce-bound replies from curl
|
||||
inside MXC through the authenticated proxy. Its `/32` allowlist and explicit
|
||||
GET paths permit only the intended service; a denied path must yield HTTP 403
|
||||
(curl exit 22) and never reach the host request log. Host-side readiness and
|
||||
request proofs stay outside the workload's writable directory. The service is
|
||||
stopped even after failed runs. Use `-Scenario host-service` for focused checks.
|
||||
No Internet access or firewall modification is required; an absent usable host
|
||||
IPv4 address or failed host health check is a failure, not a successful denial.
|
||||
Loopback remains blocked by the shared proxy, so do not broaden SSRF controls
|
||||
to run this test.
|
||||
|
||||
When reporting `windows:ci`, distinguish these categories:
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ explicitly loads the boundary-injected CA bundle and records the peer certificat
|
||||
success requires a nonempty response and the OpenShell proxy CA issuer. It does
|
||||
not install a host trust root or disable CA/hostname verification. The fixture
|
||||
uses best-effort Schannel revocation because the ephemeral private CA has no CRL
|
||||
distribution point; it does not qualify revocation enforcement. The default six E2E
|
||||
distribution point; it does not qualify revocation enforcement. The default seven E2E
|
||||
scenarios remain independent of Internet access.
|
||||
|
||||
The `forwarding` scenario starts a TCP listener inside MXC, then verifies two
|
||||
@@ -287,12 +287,22 @@ unique GET responses through gateway host-based routing. It deletes the endpoint
|
||||
and requires HTTP 404 afterward. Requests connect only to the gateway with the
|
||||
exposed URL's Host header, so no wildcard DNS setup is required.
|
||||
|
||||
The default `host-service` scenario runs an HTTP fixture on the Windows host's
|
||||
active LAN IPv4 address and a dynamic port. A real MXC workload uses curl through
|
||||
the authenticated supervisor proxy, with that exact address allowed by a `/32`
|
||||
policy. Two GETs must return the fixture's nonce; a disallowed path must return
|
||||
HTTP 403 with curl exit 22 and never appear in the host-side request log. The
|
||||
host service is checked before launch and stopped after testing. Missing host
|
||||
IPv4 connectivity fails the scenario. This uses the host's LAN address because
|
||||
the shared proxy always rejects loopback destinations. Select it with
|
||||
`-Scenario host-service`; no Internet access or firewall changes are required.
|
||||
|
||||
The `ws-agent.yaml` and `openclaw-gateway.yaml` policies preserve the Windows
|
||||
branch's workload grants. `inference.yaml` is a template: replace its filesystem,
|
||||
curl executable, inference host, and port placeholders before use. Authorize
|
||||
curl, which owns the network socket, not its cmd launcher. These policies have
|
||||
parser/mapper regression coverage; they do not add live WebSocket, OpenClaw,
|
||||
or provider-backed inference scenarios to the six-scenario E2E runner.
|
||||
or provider-backed inference scenarios to the seven-scenario E2E runner.
|
||||
|
||||
With `-KeepRunning`, the runner restores the caller's environment but retains
|
||||
the disposable CLI/runtime directory for the live gateway and prints its path.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
version: 1
|
||||
filesystem_policy:
|
||||
include_workdir: false
|
||||
read_only: []
|
||||
read_write:
|
||||
- "C:/work/openshell-mxc-e2e"
|
||||
network_policies:
|
||||
host_service:
|
||||
name: host-service
|
||||
endpoints:
|
||||
- host: '__HOST_SERVICE_IP__'
|
||||
port: __HOST_SERVICE_PORT__
|
||||
allowed_ips: ['__HOST_SERVICE_IP__/32']
|
||||
protocol: rest
|
||||
enforcement: enforce
|
||||
rules:
|
||||
- allow:
|
||||
method: GET
|
||||
path: /allowed/*
|
||||
binaries:
|
||||
- path: '__CURL_EXE__'
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Test-only TCP fixture for real MXC ingress E2E; not a shipped runtime binary.
|
||||
//! Test-only TCP/HTTP fixture for real MXC E2E; not a shipped runtime binary.
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::TcpListener;
|
||||
@@ -9,14 +9,25 @@ use std::time::Duration;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let arguments: Vec<String> = std::env::args().collect();
|
||||
if !(arguments.len() == 3 || (arguments.len() == 4 && arguments[3] == "http")) {
|
||||
if !(arguments.len() == 3 || (matches!(arguments.len(), 4 | 6) && arguments[3] == "http")) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"usage: mxc-forwarding-agent READY_FILE NONCE [http]",
|
||||
"usage: mxc-forwarding-agent READY_FILE NONCE [http [BIND_IPV4 REQUEST_LOG]]",
|
||||
));
|
||||
}
|
||||
let http_mode = arguments.len() == 4;
|
||||
let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))?;
|
||||
let http_mode = arguments.len() >= 4;
|
||||
let bind_ip = if arguments.len() == 6 {
|
||||
arguments[4]
|
||||
.parse::<std::net::Ipv4Addr>()
|
||||
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?
|
||||
} else {
|
||||
std::net::Ipv4Addr::LOCALHOST
|
||||
};
|
||||
let request_log = arguments.get(5);
|
||||
if let Some(path) = request_log {
|
||||
std::fs::write(path, "")?;
|
||||
}
|
||||
let listener = TcpListener::bind((bind_ip, 0))?;
|
||||
let ready_file = std::path::Path::new(&arguments[1]);
|
||||
let staged_ready = ready_file.with_extension("tmp");
|
||||
std::fs::write(&staged_ready, listener.local_addr()?.port().to_string())?;
|
||||
@@ -59,6 +70,10 @@ fn main() -> std::io::Result<()> {
|
||||
let target = fields.next().ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "missing HTTP target")
|
||||
})?;
|
||||
if let Some(path) = request_log {
|
||||
let mut log = std::fs::OpenOptions::new().append(true).open(path)?;
|
||||
writeln!(log, "{target}")?;
|
||||
}
|
||||
let body = format!("{}:{target}", arguments[2]);
|
||||
write!(
|
||||
socket,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
# network-policy - supervisor-owned network policy permits admission.
|
||||
# forwarding - two TCP request/reply exchanges through OpenShell ingress.
|
||||
# service-forwarding - named HTTP service routing and endpoint deletion.
|
||||
# host-service - workload reaches a host HTTP service; denied path never arrives.
|
||||
# https-ca - opt-in outbound HTTPS using the injected proxy CA.
|
||||
|
||||
[CmdletBinding()]
|
||||
@@ -384,6 +385,46 @@ function Wait-File([string]$path, [int]$seconds) {
|
||||
return (Test-Path $path)
|
||||
}
|
||||
|
||||
function Get-HostServiceAddress {
|
||||
foreach ($adapter in [Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()) {
|
||||
if ($adapter.OperationalStatus -ne [Net.NetworkInformation.OperationalStatus]::Up) { continue }
|
||||
foreach ($unicast in $adapter.GetIPProperties().UnicastAddresses) {
|
||||
$ip = $unicast.Address
|
||||
if ($ip.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetwork -and
|
||||
-not [Net.IPAddress]::IsLoopback($ip) -and
|
||||
-not $ip.ToString().StartsWith('169.254.') -and $ip.ToString() -ne '0.0.0.0') {
|
||||
return $ip.ToString()
|
||||
}
|
||||
}
|
||||
}
|
||||
throw 'host-service requires an active host IPv4 address outside loopback and link-local ranges'
|
||||
}
|
||||
|
||||
function Test-HostServiceResult([string]$work, [string]$nonce, [string]$requestLog) {
|
||||
if (-not (Wait-File (Join-Path $work 'host-service.done') 60)) { throw 'host-service workload did not complete' }
|
||||
if ((Get-Content -LiteralPath (Join-Path $work 'host-service.done') -Raw).Trim() -ne $nonce) {
|
||||
throw 'host-service completion marker does not match this workload'
|
||||
}
|
||||
foreach ($name in @('one', 'two')) {
|
||||
$body = Get-Content -LiteralPath (Join-Path $work "host-service.$name.body") -Raw
|
||||
$exitCode = (Get-Content -LiteralPath (Join-Path $work "host-service.$name.exit") -Raw).Trim()
|
||||
if ($exitCode -ne '0' -or $body -ne "${nonce}:/allowed/$name") {
|
||||
throw "host-service allowed request $name did not return the exact host response"
|
||||
}
|
||||
}
|
||||
$deniedExit = (Get-Content -LiteralPath (Join-Path $work 'host-service.denied.exit') -Raw).Trim()
|
||||
$deniedStatus = (Get-Content -LiteralPath (Join-Path $work 'host-service.denied.status') -Raw).Trim()
|
||||
if ($deniedExit -ne '22' -or $deniedStatus -ne '403') {
|
||||
throw "host-service denied request must receive policy HTTP 403 (curl exit 22), got $deniedStatus / $deniedExit"
|
||||
}
|
||||
$requests = @(Get-Content -LiteralPath $requestLog)
|
||||
if ($requests.Count -ne 3 -or $requests[0] -ne '/health' -or
|
||||
$requests[1] -ne '/allowed/one' -or $requests[2] -ne '/allowed/two') {
|
||||
throw "host-service received unexpected requests: $($requests -join ', ')"
|
||||
}
|
||||
return 'two exact host responses; denied request returned HTTP 403 and never reached the service'
|
||||
}
|
||||
|
||||
# Exercise the public gRPC forward path, never dial the workload port directly.
|
||||
# The nonce identifies this workload, not an unrelated host-loopback listener.
|
||||
function Test-Forwarding([string]$sandboxName, [string]$readyFile, [string]$nonce) {
|
||||
@@ -610,6 +651,7 @@ function Probe-Backend([string] $backendName, [string] $wxc) {
|
||||
|
||||
$results = @()
|
||||
$gw = $null
|
||||
$hostService = $null
|
||||
$harnessError = $null
|
||||
$backendProbe = @{ Live = $false; Reason = "not probed" }
|
||||
# Unique per-run suffix so a stale sandbox record from an earlier run can never
|
||||
@@ -750,10 +792,15 @@ try {
|
||||
Backends = "process_container"; Kind = "service-forwarding"
|
||||
ReadyFile = (Join-Path $DemoDir "service-forwarding-port.txt")
|
||||
Description = "named HTTP service routes two requests into MXC and stops routing after deletion"
|
||||
},
|
||||
@{
|
||||
Name = 'host-service'; PolicyFile = Join-Path $policyDir 'host-service.yaml'
|
||||
SandboxId = 'host'; Backends = 'process_container'; Kind = 'host-service'
|
||||
Description = 'workload HTTP egress reaches a host service only for explicitly allowed paths'
|
||||
}
|
||||
)
|
||||
|
||||
# External connectivity is deliberately opt-in; the default six scenarios
|
||||
# External connectivity is deliberately opt-in; the default seven scenarios
|
||||
# remain runnable without Internet access. Use the same authenticated
|
||||
# gateway launch path as every other workload, not a bare driver fixture.
|
||||
if ($Scenario -eq 'https-ca') {
|
||||
@@ -826,6 +873,30 @@ try {
|
||||
$DemoDir.Replace('\', '/')
|
||||
)
|
||||
$curlExe = [IO.Path]::Combine([IO.Path]::GetDirectoryName($cmdExe), 'curl.exe')
|
||||
if ($sc.Kind -eq 'host-service') {
|
||||
$hostIp = Get-HostServiceAddress
|
||||
$hostNonce = [guid]::NewGuid().ToString('N')
|
||||
# The workload has write access only to DemoDir, not these host-side proofs.
|
||||
$hostReady = Join-Path $resultDir 'host-service.port'
|
||||
$hostRequests = Join-Path $resultDir 'host-service.requests'
|
||||
$fixture = Join-Path $BinaryDir 'mxc-forwarding-agent.exe'
|
||||
if (-not (Test-Path -LiteralPath $fixture)) { $fixture = Join-Path $BinaryDir 'examples/mxc-forwarding-agent.exe' }
|
||||
if (-not (Test-Path -LiteralPath $fixture)) { throw 'Missing host-service fixture; run windows:build:mxc-fixtures' }
|
||||
$hostArguments = @($hostReady, $hostNonce, 'http', $hostIp, $hostRequests)
|
||||
$hostService = Start-Process -FilePath $fixture -PassThru -WindowStyle Hidden `
|
||||
-ArgumentList (($hostArguments | ForEach-Object { Quote-NativeArgument $_ }) -join ' ') `
|
||||
-RedirectStandardOutput (Join-Path $resultDir 'host-service.stdout.log') `
|
||||
-RedirectStandardError (Join-Path $resultDir 'host-service.stderr.log')
|
||||
if (-not (Wait-File $hostReady 10) -or $hostService.HasExited) { throw 'host-service fixture did not start' }
|
||||
$hostPort = [int](Get-Content -LiteralPath $hostReady -Raw).Trim()
|
||||
$hostUrl = "http://${hostIp}:$hostPort"
|
||||
$health = Invoke-NativeCaptured $curlExe @('--disable', '--proxy', '', '--noproxy', '*',
|
||||
'--fail', '--silent', '--show-error', '--max-time', '10', "$hostUrl/health") 15
|
||||
if ($health.ExitCode -ne 0 -or ($health.Output -join '').Trim() -ne "${hostNonce}:/health") {
|
||||
throw 'host-service health check failed before sandbox launch'
|
||||
}
|
||||
$policyText = $policyText.Replace('__HOST_SERVICE_IP__', $hostIp).Replace('__HOST_SERVICE_PORT__', "$hostPort")
|
||||
}
|
||||
# Preserve the exact Windows executable path used in the launch;
|
||||
# network binary identities are compared as strings, not FS paths.
|
||||
$policyText = $policyText.Replace('__CURL_EXE__', $curlExe.Replace("'", "''"))
|
||||
@@ -852,6 +923,20 @@ try {
|
||||
} else {
|
||||
$command = @($cmdExe, "/c", "echo denied 1> $denied")
|
||||
}
|
||||
} elseif ($sc.Kind -eq 'host-service') {
|
||||
Remove-Item -Path (Join-Path $DemoDir 'host-service.*') -Force -ErrorAction SilentlyContinue
|
||||
$batch = Join-Path $DemoDir 'host-service.cmd'
|
||||
$lines = @('@echo off')
|
||||
foreach ($name in @('one', 'two')) {
|
||||
$lines += '"{0}" --disable --proxy "%HTTP_PROXY%" --noproxy "" --fail --silent --show-error --max-time 15 --output "{1}" "{2}/allowed/{3}"' -f $curlExe, (Join-Path $DemoDir "host-service.$name.body"), $hostUrl, $name
|
||||
$lines += 'echo %errorlevel%> "{0}"' -f (Join-Path $DemoDir "host-service.$name.exit")
|
||||
}
|
||||
$lines += '"{0}" --disable --proxy "%HTTP_PROXY%" --noproxy "" --fail --silent --show-error --max-time 15 --output "{1}" --write-out "%%{{http_code}}" "{2}/denied" > "{3}"' -f $curlExe, (Join-Path $DemoDir 'host-service.denied.body'), $hostUrl, (Join-Path $DemoDir 'host-service.denied.status')
|
||||
$lines += 'echo %errorlevel%> "{0}"' -f (Join-Path $DemoDir 'host-service.denied.exit')
|
||||
$lines += 'echo {0}> "{1}"' -f $hostNonce, (Join-Path $DemoDir 'host-service.done')
|
||||
$lines += 'exit /b 0'
|
||||
Set-Content -LiteralPath $batch -Value ($lines -join "`r`n") -Encoding ASCII
|
||||
$command = @($cmdExe, '/d', '/c', $batch)
|
||||
} elseif ($sc.Kind -eq 'socket-owner') {
|
||||
Remove-Item -LiteralPath $sc.PosTarget, $sc.ControlTarget, $sc.ErrorTarget -Force -ErrorAction SilentlyContinue
|
||||
$child = 'echo launched > "{0}" && "{1}" --disable --fail --silent --show-error --max-time 30 --ssl-revoke-best-effort --cacert "%CURL_CA_BUNDLE%" --stderr "{2}" --output "{3}" https://example.com/' -f $sc.ControlTarget, $curlExe, $sc.ErrorTarget, $sc.PosTarget
|
||||
@@ -916,12 +1001,13 @@ try {
|
||||
# Run sandbox create; require artifact evidence for live workload tests.
|
||||
$createOut = $null; $createExitCode = 0
|
||||
try {
|
||||
$createResult = Invoke-NativeCaptured $cli @(
|
||||
$createArguments = @(
|
||||
"sandbox", "create", "--name", $sandboxName,
|
||||
"--policy", [string]$policyUsed,
|
||||
"--driver-config-json", $driverConfig,
|
||||
"--no-tty"
|
||||
"--driver-config-json", $driverConfig
|
||||
)
|
||||
$createArguments += if ($sc.Kind -eq 'host-service') { '--detach' } else { '--no-tty' }
|
||||
$createResult = Invoke-NativeCaptured $cli $createArguments
|
||||
$createOut = $createResult.Output
|
||||
$createExitCode = $createResult.ExitCode
|
||||
} catch {
|
||||
@@ -933,7 +1019,18 @@ try {
|
||||
$gwText = (Get-Content $gwLog, $gwErrLog -Raw -ErrorAction SilentlyContinue) -join "`n"
|
||||
|
||||
# Evaluate.
|
||||
if ($sc.Kind -eq 'socket-owner') {
|
||||
if ($sc.Kind -eq 'host-service') {
|
||||
try {
|
||||
if ($createExitCode -ne 0) { throw "host-service sandbox creation failed: $createOutStr" }
|
||||
if ($hostService.HasExited) { throw 'host-service fixture exited before scoring' }
|
||||
$reason = Test-HostServiceResult $DemoDir $hostNonce $hostRequests
|
||||
Ok "host-service: $reason"
|
||||
$results += [pscustomobject]@{ Scenario = $sc.Name; Result = 'PASS'; Reason = $reason }
|
||||
} catch {
|
||||
Bad "host-service: $($_.Exception.Message)"
|
||||
$results += [pscustomobject]@{ Scenario = $sc.Name; Result = 'FAIL'; Reason = $_.Exception.Message }
|
||||
}
|
||||
} elseif ($sc.Kind -eq 'socket-owner') {
|
||||
try {
|
||||
if (-not (Wait-File $sc.ControlTarget 30)) { throw "parent cmd did not launch: $createOutStr" }
|
||||
$deadline = (Get-Date).AddSeconds(45)
|
||||
@@ -1079,6 +1176,10 @@ catch {
|
||||
Bad "harness error: $harnessError"
|
||||
}
|
||||
finally {
|
||||
if ($hostService -and -not $hostService.HasExited) {
|
||||
Stop-Process -Id $hostService.Id -Force -ErrorAction SilentlyContinue
|
||||
$hostService.WaitForExit()
|
||||
}
|
||||
# --- Summary + results bundle ---------------------------------------------
|
||||
Step "Summary"
|
||||
$results | Format-Table -AutoSize
|
||||
@@ -1121,6 +1222,8 @@ through OpenShell. Service forwarding verifies two HTTP replies and HTTP 404
|
||||
after endpoint deletion. Missing coverage exits non-zero; network-policy proves
|
||||
admission and execution, not network enforcement. Forwarding proves managed
|
||||
ingress works, not that direct host ingress is blocked.
|
||||
Host-service verifies two exact workload-to-host HTTP replies and HTTP 403 for
|
||||
a denied path, with a host-side request log proving that denied request never arrived.
|
||||
"@
|
||||
Set-Content -Path (Join-Path $resultDir "summary.txt") -Value $summary -Encoding UTF8
|
||||
Write-Host $summary -ForegroundColor ($(if ($verdict -eq "PASS") { "Green" } else { "Red" }))
|
||||
|
||||
@@ -66,6 +66,69 @@ fn https_fixture_authorizes_direct_curl_with_read_only_https() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_service_scoring_rejects_transport_failures_and_unenforced_requests() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let script = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$tokens = $null; $errors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
$env:OPENSHELL_MXC_RUNNER, [ref]$tokens, [ref]$errors)
|
||||
if ($errors.Count) { throw "runner parse errors: $($errors.Message -join '; ')" }
|
||||
foreach ($name in @('Wait-File', 'Test-HostServiceResult')) {
|
||||
$helper = $ast.Find({ param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
|
||||
}, $true)
|
||||
if (!$helper) { throw "missing $name" }
|
||||
Invoke-Expression $helper.Extent.Text
|
||||
}
|
||||
$work = Join-Path $env:OPENSHELL_MXC_SCORE_ROOT 'work with spaces'
|
||||
New-Item -ItemType Directory $work | Out-Null
|
||||
$requestLog = Join-Path $env:OPENSHELL_MXC_SCORE_ROOT 'host.requests'
|
||||
$nonce = 'unique-host-service-response'
|
||||
function Reset-Proofs {
|
||||
foreach ($name in @('one', 'two')) {
|
||||
[IO.File]::WriteAllText((Join-Path $work "host-service.$name.body"), "${nonce}:/allowed/$name")
|
||||
[IO.File]::WriteAllText((Join-Path $work "host-service.$name.exit"), '0')
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $work 'host-service.denied.exit'), '22')
|
||||
[IO.File]::WriteAllText((Join-Path $work 'host-service.denied.status'), '403')
|
||||
[IO.File]::WriteAllText((Join-Path $work 'host-service.done'), $nonce)
|
||||
[IO.File]::WriteAllText($requestLog, "/health`n/allowed/one`n/allowed/two`n")
|
||||
}
|
||||
Reset-Proofs
|
||||
if ((Test-HostServiceResult $work $nonce $requestLog) -notmatch 'never reached') { throw 'valid host-service run must pass' }
|
||||
foreach ($case in @('transport', 'denial-reached-host', 'wrong-response', 'wrong-workload')) {
|
||||
Reset-Proofs
|
||||
switch ($case) {
|
||||
'transport' {
|
||||
[IO.File]::WriteAllText((Join-Path $work 'host-service.denied.exit'), '7')
|
||||
[IO.File]::WriteAllText((Join-Path $work 'host-service.denied.status'), '000')
|
||||
}
|
||||
'denial-reached-host' { [IO.File]::AppendAllText($requestLog, "/denied`n") }
|
||||
'wrong-response' { [IO.File]::WriteAllText((Join-Path $work 'host-service.one.body'), 'unrelated service') }
|
||||
'wrong-workload' { [IO.File]::WriteAllText((Join-Path $work 'host-service.done'), 'stale-run') }
|
||||
}
|
||||
$rejected = $false
|
||||
try { Test-HostServiceResult $work $nonce $requestLog | Out-Null } catch { $rejected = $true }
|
||||
if (!$rejected) { throw "invalid run $case must fail" }
|
||||
}
|
||||
"#;
|
||||
let output = Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-Command", script])
|
||||
.env("OPENSHELL_MXC_RUNNER", examples().join("run-mxc-e2e.ps1"))
|
||||
.env("OPENSHELL_MXC_SCORE_ROOT", root.path())
|
||||
.output()
|
||||
.expect("launch PowerShell host-service regression");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openclaw_fixture_preserves_explicit_ui_and_node_network_grants() {
|
||||
let yaml =
|
||||
|
||||
@@ -5291,6 +5291,46 @@ process:
|
||||
OpaEngine::from_strings(TEST_POLICY, L7_TEST_DATA).expect("Failed to load L7 test data")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mxc_host_service_fixture_enforces_paths_and_socket_owner() {
|
||||
let binary = crate::test_support::executable_path("/usr/bin/curl");
|
||||
let yaml =
|
||||
include_str!("../../openshell-driver-mxc/examples/e2e-policies/host-service.yaml")
|
||||
.replace("__HOST_SERVICE_IP__", "192.168.1.20")
|
||||
.replace("__HOST_SERVICE_PORT__", "49152")
|
||||
.replace(
|
||||
"C:/work/openshell-mxc-e2e",
|
||||
&std::env::temp_dir().to_string_lossy().replace('\\', "/"),
|
||||
)
|
||||
.replace("__CURL_EXE__", binary.to_str().unwrap());
|
||||
let engine = OpaEngine::from_strings(TEST_POLICY, &yaml).expect("host-service policy");
|
||||
let network = NetworkInput {
|
||||
host: "192.168.1.20".into(),
|
||||
port: 49152,
|
||||
binary_path: binary.clone(),
|
||||
binary_sha256: "unused".into(),
|
||||
ancestors: Vec::new(),
|
||||
cmdline_paths: Vec::new(),
|
||||
};
|
||||
assert!(engine.evaluate_network(&network).unwrap().allowed);
|
||||
assert_eq!(
|
||||
engine.query_allowed_ips(&network).unwrap(),
|
||||
["192.168.1.20/32"]
|
||||
);
|
||||
for (path, allowed) in [
|
||||
("/allowed/one", true),
|
||||
("/allowed/two", true),
|
||||
("/denied", false),
|
||||
] {
|
||||
let mut input = l7_input("192.168.1.20", 49152, "GET", path);
|
||||
input["exec"]["path"] = binary.to_str().unwrap().into();
|
||||
assert_eq!(eval_l7(&engine, &input), allowed, "{path}");
|
||||
}
|
||||
let mut wrong_binary = network;
|
||||
wrong_binary.binary_path = crate::test_support::executable_path("/usr/bin/other");
|
||||
assert!(!engine.evaluate_network(&wrong_binary).unwrap().allowed);
|
||||
}
|
||||
|
||||
fn l7_input(host: &str, port: u16, method: &str, path: &str) -> serde_json::Value {
|
||||
l7_input_with_query(host, port, method, path, serde_json::json!({}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user