From 1062846a07faa26f73ed7caf955decafdd983c04 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sat, 16 May 2026 09:55:11 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0017 (20260516T052512Z-20f8) --- src/dynamic/lang/ruby.rs | 47 +++++ tests/common/fixture_harness.rs | 22 +++ .../stubs_e2e/ruby/http/vuln/main.rb | 27 +++ tests/javascript_fixtures.rs | 171 ++++++++---------- tests/stubs_e2e_per_lang.rs | 116 ++++++++++++ tests/typescript_fixtures.rs | 169 ++++++++--------- 6 files changed, 366 insertions(+), 186 deletions(-) create mode 100644 tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index 945c4187..8e2ee106 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -279,6 +279,32 @@ def __nyx_install_crash_guard(sink_callee) end end end + +# Phase 10 (Track D.3) HTTP recording helper. When the verifier spawned an +# HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a +# sink call site whose outbound request never reaches the on-the-wire +# listener (DNS-mocked, network-isolated sandbox, pre-flight check) can +# call this helper to surface the attempted call. Format matches the +# Python / Node / PHP / Go siblings so the host-side HttpStub log-line +# merger parses all five streams identically. No-op when NYX_HTTP_LOG is +# unset so the same harness still runs cleanly under modes that did not +# spawn a stub. Single-quoted Ruby string literals keep this helper free +# of the literal hash-after-double-quote sequence that would terminate +# the surrounding Rust raw string. +def __nyx_stub_http_record(method, url, body = nil, **detail) + p = ENV['NYX_HTTP_LOG'] + return if p.nil? || p.empty? + begin + File.open(p, 'a') do |f| + f.puts('# method: ' + method.to_s) + f.puts('# url: ' + url.to_s) + f.puts('# body: ' + body.to_s) unless body.nil? + detail.each { |k, v| f.puts('# ' + k.to_s + ': ' + v.to_s) } + f.puts(method.to_s + ' ' + url.to_s) + end + rescue StandardError + end +end "# } @@ -778,6 +804,27 @@ mod tests { ); } + #[test] + fn probe_shim_publishes_stub_http_recorder() { + let shim = probe_shim(); + assert!( + shim.contains("def __nyx_stub_http_record"), + "Ruby probe shim must define __nyx_stub_http_record" + ); + assert!( + shim.contains("ENV['NYX_HTTP_LOG']"), + "Ruby HTTP recorder must read NYX_HTTP_LOG to find the side-channel log" + ); + assert!( + shim.contains("# method: "), + "Ruby HTTP recorder must emit a hash-prefixed method detail line" + ); + assert!( + shim.contains("# url: "), + "Ruby HTTP recorder must emit a hash-prefixed url detail line" + ); + } + #[test] fn chain_step_splices_probe_shim_for_composite_reverify() { let step = chain_step(Some(b"")); diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs index 6bf18df7..06fc9031 100644 --- a/tests/common/fixture_harness.rs +++ b/tests/common/fixture_harness.rs @@ -68,6 +68,12 @@ pub enum Prerequisite { /// A static C library archive (e.g. `libc.a`) must be linkable. /// Used by the Phase-17/20 hardening probe fixtures. StaticLib(&'static str), + /// A Node.js module must be importable via `require.resolve`. Used + /// by the JavaScript / TypeScript framework-bound shape suites + /// (express / koa / next / jsdom) so a host without the package on + /// the resolution path skips with a structured reason instead of + /// failing the test. + NodeModuleAvailable(&'static str), } /// Phase 29 (Track I): why the harness skipped a fixture. Carried by @@ -80,6 +86,7 @@ pub enum SkipReason { MissingEnvVar(&'static str), DockerUnavailable, MissingStaticLib(&'static str), + MissingNodeModule(&'static str), } impl std::fmt::Display for SkipReason { @@ -89,6 +96,9 @@ impl std::fmt::Display for SkipReason { SkipReason::MissingEnvVar(v) => write!(f, "env var not set: {v}"), SkipReason::DockerUnavailable => write!(f, "docker daemon unavailable"), SkipReason::MissingStaticLib(l) => write!(f, "static lib not linkable: {l}"), + SkipReason::MissingNodeModule(m) => { + write!(f, "Node module not resolvable via require.resolve: {m}") + } } } } @@ -125,6 +135,18 @@ pub fn check_prerequisites(reqs: &[Prerequisite]) -> Result<(), SkipReason> { return Err(SkipReason::DockerUnavailable); } } + Prerequisite::NodeModuleAvailable(name) => { + let probe = format!("require.resolve('{name}')"); + let ok = std::process::Command::new("node") + .arg("-e") + .arg(&probe) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if !ok { + return Err(SkipReason::MissingNodeModule(name)); + } + } Prerequisite::StaticLib(lib) => { // Treat the lib as linkable iff `cc -static -l` on // an empty TU succeeds. Slow but reliable; only called diff --git a/tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb b/tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb new file mode 100644 index 00000000..e5e30f1b --- /dev/null +++ b/tests/dynamic_fixtures/stubs_e2e/ruby/http/vuln/main.rb @@ -0,0 +1,27 @@ +# Phase 10 (Track D.3) stub-end-to-end fixture: Ruby + HTTP. +# +# The verifier publishes: +# +# * NYX_HTTP_ENDPOINT — http://127.0.0.1:{port} the HttpStub listens on. +# * NYX_HTTP_LOG — companion log path the harness appends attempted +# outbound calls to so the host HttpStub picks them up on +# drain_events() even when the request bypasses the on-the-wire +# listener (DNS-mocked, network-isolated sandbox, pre-flight check). +# +# This fixture exercises the side-channel path: it records an attempted +# SSRF call to http://169.254.169.254/latest/meta-data/ through the +# Ruby shim helper __nyx_stub_http_record without issuing the actual +# network call. The companion test in tests/stubs_e2e_per_lang.rs +# splices in nyx_scanner::dynamic::lang::ruby::probe_shim ahead of this +# source, runs it with both env vars set, and asserts the stub captured +# the attempt. + +method = 'GET' +url = 'http://169.254.169.254/latest/meta-data/' +body = '' +# Record the attempted call through the probe shim so the host +# HttpStub captures it on the next drain_events() call even when the +# harness never reaches the on-the-wire listener. +__nyx_stub_http_record(method, url, body, driver: 'net/http') +# Echo so the host can confirm the driver ran end-to-end. +$stdout.puts(ENV['NYX_HTTP_ENDPOINT'] || 'no-endpoint') diff --git a/tests/javascript_fixtures.rs b/tests/javascript_fixtures.rs index 2d884fb9..c88c9744 100644 --- a/tests/javascript_fixtures.rs +++ b/tests/javascript_fixtures.rs @@ -18,28 +18,18 @@ mod common; #[cfg(feature = "dynamic")] mod javascript_fixture_tests { - use crate::common::fixture_harness::run_shape_fixture_lang; + use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; - fn node_available() -> bool { - std::process::Command::new("node") - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - } - - fn node_module_available(name: &'static str) -> bool { - std::process::Command::new("node") - .arg("-e") - .arg(format!("require.resolve('{name}')")) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - } + /// Base prereq slice shared by every JS shape: the host must have + /// `node` on PATH. Framework-bound shapes extend the slice with a + /// second `Prerequisite::NodeModuleAvailable("")` entry so a + /// host without the package on the resolution path skips with a + /// structured reason rather than failing the test. + const NODE_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailable("node")]; fn assert_confirmed(shape: &str, result: &VerifyResult) { assert_eq!( @@ -68,7 +58,9 @@ mod javascript_fixture_tests { ); } + #[allow(clippy::too_many_arguments)] fn run( + requires: &[Prerequisite], shape: &str, file: &str, func: &str, @@ -76,8 +68,9 @@ mod javascript_fixture_tests { sink_line: u32, kind: EntryKind, slot: PayloadSlot, - ) -> VerifyResult { - run_shape_fixture_lang( + ) -> Option { + run_shape_fixture_lang_or_skip( + requires, Lang::JavaScript, "javascript", shape, @@ -94,21 +87,21 @@ mod javascript_fixture_tests { #[test] fn commonjs_export_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "commonjs_export", "vuln.js", "runPing", Cap::CODE_EXEC, 11, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("commonjs_export", &r); } #[test] fn commonjs_export_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "commonjs_export", "benign.js", "runPing", Cap::CODE_EXEC, 11, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("commonjs_export", &r); } @@ -116,21 +109,21 @@ mod javascript_fixture_tests { #[test] fn async_function_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "async_function", "vuln.js", "runPing", Cap::CODE_EXEC, 15, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("async_function", &r); } #[test] fn async_function_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "async_function", "benign.js", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("async_function", &r); } @@ -138,21 +131,21 @@ mod javascript_fixture_tests { #[test] fn esm_default_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "esm_default", "vuln.js", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("esm_default", &r); } #[test] fn esm_default_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "esm_default", "benign.js", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("esm_default", &r); } @@ -160,29 +153,27 @@ mod javascript_fixture_tests { #[test] fn express_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("express") { - eprintln!("SKIP: express not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("express"), + ], "express", "vuln.js", "ping", Cap::CODE_EXEC, 15, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("express", &r); } #[test] fn express_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("express") { - eprintln!("SKIP: express not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("express"), + ], "express", "benign.js", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("express", &r); } @@ -190,29 +181,27 @@ mod javascript_fixture_tests { #[test] fn koa_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("koa") { - eprintln!("SKIP: koa not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("koa"), + ], "koa", "vuln.js", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("koa", &r); } #[test] fn koa_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("koa") { - eprintln!("SKIP: koa not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("koa"), + ], "koa", "benign.js", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("koa", &r); } @@ -220,29 +209,27 @@ mod javascript_fixture_tests { #[test] fn next_route_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("next") { - eprintln!("SKIP: next not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("next"), + ], "next_route", "vuln.js", "handler", Cap::CODE_EXEC, 17, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("next_route", &r); } #[test] fn next_route_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("next") { - eprintln!("SKIP: next not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("next"), + ], "next_route", "benign.js", "handler", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("next_route", &r); } @@ -250,29 +237,27 @@ mod javascript_fixture_tests { #[test] fn browser_event_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("jsdom") { - eprintln!("SKIP: jsdom not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("jsdom"), + ], "browser_event", "vuln.js", "clickHandler", Cap::HTML_ESCAPE, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("browser_event", &r); } #[test] fn browser_event_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("jsdom") { - eprintln!("SKIP: jsdom not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("jsdom"), + ], "browser_event", "benign.js", "clickHandler", Cap::HTML_ESCAPE, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("browser_event", &r); } } diff --git a/tests/stubs_e2e_per_lang.rs b/tests/stubs_e2e_per_lang.rs index 88f7b5f5..9d6f132b 100644 --- a/tests/stubs_e2e_per_lang.rs +++ b/tests/stubs_e2e_per_lang.rs @@ -24,6 +24,7 @@ use nyx_scanner::dynamic::lang::go::probe_shim as go_probe_shim; use nyx_scanner::dynamic::lang::javascript::probe_shim as node_probe_shim; use nyx_scanner::dynamic::lang::php::probe_shim as php_probe_shim; use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim; +use nyx_scanner::dynamic::lang::ruby::probe_shim as ruby_probe_shim; use nyx_scanner::dynamic::stubs::{HttpStub, SqlStub, StubProvider}; use std::path::PathBuf; use std::process::Command; @@ -61,6 +62,14 @@ fn go_available() -> bool { .unwrap_or(false) } +fn ruby_available() -> bool { + Command::new("ruby") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + /// Wrap the body-only Go HTTP fixture in a complete `package main` /// program: stdlib imports needed by the spliced probe shim plus the /// fragment's own `fmt` / `os` references, the shim itself, and the @@ -793,6 +802,113 @@ fn go_http_shim_recorder_is_noop_without_log_env() { ); } +#[test] +fn ruby_http_stub_captures_attempted_outbound_via_shim_recorder() { + // Phase 10 (Track D.3) HTTP recording: Ruby leg of the side-channel + // `__nyx_stub_http_record` helper. Mirrors the Python HTTP test — + // records an SSRF attempt without issuing the actual network call. + // Ruby has no package / class boundary so the fixture is a plain + // top-level script and the shim is prepended at the file head. + if !ruby_available() { + eprintln!("SKIP: ruby not available"); + return; + } + + let workdir = TempDir::new().expect("tempdir"); + let stub = HttpStub::start(workdir.path()).expect("HttpStub::start"); + + let endpoint = stub.endpoint(); + let recording = stub + .recording_endpoint() + .expect("HttpStub must publish a recording endpoint"); + + let fixture = + std::fs::read_to_string(fixture_path("ruby/http/vuln/main.rb")).expect("read fixture"); + let mut combined = String::with_capacity(ruby_probe_shim().len() + fixture.len() + 64); + combined.push_str(ruby_probe_shim()); + combined.push_str("\n# ── fixture begins ─\n"); + combined.push_str(&fixture); + + let script_path = workdir.path().join("driver_http.rb"); + std::fs::write(&script_path, combined).expect("write driver"); + + let output = Command::new("ruby") + .arg(&script_path) + .env("NYX_HTTP_ENDPOINT", &endpoint) + .env(recording.0, &recording.1) + .output() + .expect("ruby driver"); + assert!( + output.status.success(), + "driver must exit 0; stderr = {}", + String::from_utf8_lossy(&output.stderr) + ); + + let events = stub.drain_events(); + assert!( + !events.is_empty(), + "HttpStub must capture at least one event after the Ruby shim recorder fires" + ); + let hit = events + .iter() + .find(|e| e.summary.contains("169.254.169.254")) + .expect("recorded URL must contain the SSRF marker"); + assert_eq!( + hit.detail.get("method").map(String::as_str), + Some("GET"), + "method detail must surface on the recorded event" + ); + assert_eq!( + hit.detail.get("url").map(String::as_str), + Some("http://169.254.169.254/latest/meta-data/"), + ); + assert_eq!( + hit.detail.get("driver").map(String::as_str), + Some("net/http"), + "kwargs passed to __nyx_stub_http_record must surface as event detail entries" + ); +} + +#[test] +fn ruby_http_shim_recorder_is_noop_without_log_env() { + if !ruby_available() { + eprintln!("SKIP: ruby not available"); + return; + } + + let workdir = TempDir::new().expect("tempdir"); + let stub = HttpStub::start(workdir.path()).expect("HttpStub::start"); + + let endpoint = stub.endpoint(); + let fixture = + std::fs::read_to_string(fixture_path("ruby/http/vuln/main.rb")).expect("read fixture"); + let mut combined = String::new(); + combined.push_str(ruby_probe_shim()); + combined.push('\n'); + combined.push_str(&fixture); + let script_path = workdir.path().join("driver_http_no_log.rb"); + std::fs::write(&script_path, combined).expect("write driver"); + + let output = Command::new("ruby") + .arg(&script_path) + .env("NYX_HTTP_ENDPOINT", &endpoint) + .env_remove("NYX_HTTP_LOG") + .output() + .expect("ruby driver"); + assert!( + output.status.success(), + "driver must exit 0 even without NYX_HTTP_LOG; stderr = {}", + String::from_utf8_lossy(&output.stderr) + ); + + let events = stub.drain_events(); + assert!( + events.is_empty(), + "no events expected when the recording env var is unset, got {} entries", + events.len() + ); +} + #[test] fn node_sql_shim_recorder_is_noop_without_log_env() { if !node_available() { diff --git a/tests/typescript_fixtures.rs b/tests/typescript_fixtures.rs index a6a34ba8..2e54029a 100644 --- a/tests/typescript_fixtures.rs +++ b/tests/typescript_fixtures.rs @@ -10,28 +10,16 @@ mod common; #[cfg(feature = "dynamic")] mod typescript_fixture_tests { - use crate::common::fixture_harness::run_shape_fixture_lang; + use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; - fn node_available() -> bool { - std::process::Command::new("node") - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - } - - fn node_module_available(name: &'static str) -> bool { - std::process::Command::new("node") - .arg("-e") - .arg(format!("require.resolve('{name}')")) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - } + /// Base prereq slice shared by every TS shape: the host must have + /// `node` on PATH. Framework-bound shapes extend the slice with a + /// second `Prerequisite::NodeModuleAvailable("")` entry. + const NODE_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailable("node")]; fn assert_confirmed(shape: &str, result: &VerifyResult) { assert_eq!( @@ -60,7 +48,9 @@ mod typescript_fixture_tests { ); } + #[allow(clippy::too_many_arguments)] fn run( + requires: &[Prerequisite], shape: &str, file: &str, func: &str, @@ -68,8 +58,9 @@ mod typescript_fixture_tests { sink_line: u32, kind: EntryKind, slot: PayloadSlot, - ) -> VerifyResult { - run_shape_fixture_lang( + ) -> Option { + run_shape_fixture_lang_or_skip( + requires, Lang::TypeScript, "typescript", shape, @@ -86,21 +77,21 @@ mod typescript_fixture_tests { #[test] fn commonjs_export_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "commonjs_export", "vuln.ts", "runPing", Cap::CODE_EXEC, 11, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("commonjs_export", &r); } #[test] fn commonjs_export_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "commonjs_export", "benign.ts", "runPing", Cap::CODE_EXEC, 11, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("commonjs_export", &r); } @@ -108,21 +99,21 @@ mod typescript_fixture_tests { #[test] fn async_function_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "async_function", "vuln.ts", "runPing", Cap::CODE_EXEC, 15, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("async_function", &r); } #[test] fn async_function_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "async_function", "benign.ts", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("async_function", &r); } @@ -130,21 +121,21 @@ mod typescript_fixture_tests { #[test] fn esm_default_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "esm_default", "vuln.ts", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("esm_default", &r); } #[test] fn esm_default_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - let r = run( + let Some(r) = run( + NODE_REQ, "esm_default", "benign.ts", "runPing", Cap::CODE_EXEC, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("esm_default", &r); } @@ -152,29 +143,27 @@ mod typescript_fixture_tests { #[test] fn express_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("express") { - eprintln!("SKIP: express not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("express"), + ], "express", "vuln.ts", "ping", Cap::CODE_EXEC, 15, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("express", &r); } #[test] fn express_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("express") { - eprintln!("SKIP: express not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("express"), + ], "express", "benign.ts", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("express", &r); } @@ -182,29 +171,27 @@ mod typescript_fixture_tests { #[test] fn koa_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("koa") { - eprintln!("SKIP: koa not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("koa"), + ], "koa", "vuln.ts", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("koa", &r); } #[test] fn koa_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("koa") { - eprintln!("SKIP: koa not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("koa"), + ], "koa", "benign.ts", "ping", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("koa", &r); } @@ -212,29 +199,27 @@ mod typescript_fixture_tests { #[test] fn next_route_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("next") { - eprintln!("SKIP: next not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("next"), + ], "next_route", "vuln.ts", "handler", Cap::CODE_EXEC, 17, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_confirmed("next_route", &r); } #[test] fn next_route_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("next") { - eprintln!("SKIP: next not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("next"), + ], "next_route", "benign.ts", "handler", Cap::CODE_EXEC, 14, EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ); + ) else { return; }; assert_not_confirmed("next_route", &r); } @@ -242,29 +227,27 @@ mod typescript_fixture_tests { #[test] fn browser_event_vuln_is_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("jsdom") { - eprintln!("SKIP: jsdom not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("jsdom"), + ], "browser_event", "vuln.ts", "clickHandler", Cap::HTML_ESCAPE, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_confirmed("browser_event", &r); } #[test] fn browser_event_benign_not_confirmed() { - if !node_available() { eprintln!("SKIP: node not available"); return; } - if !node_module_available("jsdom") { - eprintln!("SKIP: jsdom not importable"); - return; - } - let r = run( + let Some(r) = run( + &[ + Prerequisite::CommandAvailable("node"), + Prerequisite::NodeModuleAvailable("jsdom"), + ], "browser_event", "benign.ts", "clickHandler", Cap::HTML_ESCAPE, 14, EntryKind::Function, PayloadSlot::Param(0), - ); + ) else { return; }; assert_not_confirmed("browser_event", &r); } }