diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index 367037f0d..bfbd8f59b 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -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: diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index e6b23b909..fd17c1eeb 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -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. diff --git a/crates/openshell-driver-mxc/examples/e2e-policies/host-service.yaml b/crates/openshell-driver-mxc/examples/e2e-policies/host-service.yaml new file mode 100644 index 000000000..e39eb7a2f --- /dev/null +++ b/crates/openshell-driver-mxc/examples/e2e-policies/host-service.yaml @@ -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__' diff --git a/crates/openshell-driver-mxc/examples/mxc-forwarding-agent.rs b/crates/openshell-driver-mxc/examples/mxc-forwarding-agent.rs index 9afa9f8c1..55078a318 100644 --- a/crates/openshell-driver-mxc/examples/mxc-forwarding-agent.rs +++ b/crates/openshell-driver-mxc/examples/mxc-forwarding-agent.rs @@ -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 = 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::() + .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, diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 38512c25d..af2f515cc 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -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" })) diff --git a/crates/openshell-driver-mxc/tests/windows_demo_examples.rs b/crates/openshell-driver-mxc/tests/windows_demo_examples.rs index 252d85450..22ab37ffb 100644 --- a/crates/openshell-driver-mxc/tests/windows_demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/windows_demo_examples.rs @@ -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 = diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 6c28c3e11..2a95754ec 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -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!({})) }