mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0015 (20260516T052512Z-20f8)
This commit is contained in:
parent
6a169f51b8
commit
f701b43152
6 changed files with 459 additions and 120 deletions
|
|
@ -562,6 +562,86 @@ pub fn run_shape_fixture_lang(
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 29 (Track I) — `run_shape_fixture_lang` with structured
|
||||
/// prerequisite gating.
|
||||
///
|
||||
/// Checks `requires` against the host before staging the fixture; when
|
||||
/// a prerequisite is unmet, eprintln-skips with a [`SkipReason`] (so
|
||||
/// `cargo nextest` surfaces the line in test output) and returns
|
||||
/// `None`. Callers migrate from the bespoke
|
||||
/// `python3_available()` / `go_available()` / etc. helpers + per-test
|
||||
/// `eprintln!("SKIP ...") ; return;` blocks to a single
|
||||
/// `let Some(r) = run_shape_fixture_lang_or_skip(...) else { return; };`
|
||||
/// at the call site.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(dead_code)]
|
||||
pub fn run_shape_fixture_lang_or_skip(
|
||||
requires: &[Prerequisite],
|
||||
lang: nyx_scanner::symbol::Lang,
|
||||
lang_dir: &str,
|
||||
shape_dir: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) -> Option<VerifyResult> {
|
||||
if let Err(reason) = check_prerequisites(requires) {
|
||||
eprintln!("SKIP {lang_dir}/{shape_dir}/{file}: {reason}");
|
||||
return None;
|
||||
}
|
||||
Some(run_shape_fixture_lang(
|
||||
lang,
|
||||
lang_dir,
|
||||
shape_dir,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
entry_kind,
|
||||
payload_slot,
|
||||
))
|
||||
}
|
||||
|
||||
/// Phase 29 (Track I) — `run_harness_snapshot_lang` with structured
|
||||
/// prerequisite gating. Returns `false` and eprintln-skips when a
|
||||
/// prerequisite is unmet; otherwise runs the snapshot to completion
|
||||
/// and returns `true`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(dead_code)]
|
||||
pub fn run_harness_snapshot_lang_or_skip(
|
||||
requires: &[Prerequisite],
|
||||
lang: nyx_scanner::symbol::Lang,
|
||||
lang_dir: &str,
|
||||
snapshot_ext: &str,
|
||||
shape_dir: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
entry_kind: EntryKind,
|
||||
payload_slot: nyx_scanner::dynamic::spec::PayloadSlot,
|
||||
) -> bool {
|
||||
if let Err(reason) = check_prerequisites(requires) {
|
||||
eprintln!("SKIP {lang_dir}/{shape_dir}/{file}: {reason}");
|
||||
return false;
|
||||
}
|
||||
run_harness_snapshot_lang(
|
||||
lang,
|
||||
lang_dir,
|
||||
snapshot_ext,
|
||||
shape_dir,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
entry_kind,
|
||||
payload_slot,
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
/// Phase 12 — Python-specific harness snapshot wrapper.
|
||||
///
|
||||
/// Pins lang to [`Lang::Python`] and the lang dir to `python` so legacy
|
||||
|
|
|
|||
31
tests/dynamic_fixtures/stubs_e2e/node/http/vuln/main.js
Normal file
31
tests/dynamic_fixtures/stubs_e2e/node/http/vuln/main.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Phase 10 (Track D.3) stub-end-to-end fixture: Node + 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 Node
|
||||
// shim helper __nyx_stub_http_record without issuing the actual network
|
||||
// call. The companion test in tests/stubs_e2e_per_lang.rs splices in
|
||||
// crate::dynamic::lang::javascript::probe_shim ahead of this source, runs
|
||||
// it with both env vars set, and asserts the stub captured the attempt.
|
||||
|
||||
function main() {
|
||||
const method = 'GET';
|
||||
const url = 'http://169.254.169.254/latest/meta-data/';
|
||||
const 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: 'node:http' });
|
||||
// Echo so the host can confirm the driver ran end-to-end.
|
||||
console.log(process.env.NYX_HTTP_ENDPOINT || 'no-endpoint');
|
||||
}
|
||||
|
||||
main();
|
||||
35
tests/dynamic_fixtures/stubs_e2e/php/http/vuln/main.php
Normal file
35
tests/dynamic_fixtures/stubs_e2e/php/http/vuln/main.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
// Phase 10 (Track D.3) stub-end-to-end fixture: PHP + 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 PHP
|
||||
// shim helper __nyx_stub_http_record without issuing the actual network
|
||||
// call. The companion test in tests/stubs_e2e_per_lang.rs strips this
|
||||
// leading <?php tag, splices in crate::dynamic::lang::php::probe_shim
|
||||
// ahead of the remaining body inside a fresh <?php block, runs it with
|
||||
// both env vars set, and asserts the stub captured the attempt.
|
||||
|
||||
function nyx_e2e_main(): void {
|
||||
$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' => 'curl']);
|
||||
// Echo so the host can confirm the driver ran end-to-end.
|
||||
$endpoint = getenv('NYX_HTTP_ENDPOINT');
|
||||
echo ($endpoint === false || $endpoint === '') ? 'no-endpoint' : $endpoint;
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
nyx_e2e_main();
|
||||
|
|
@ -13,20 +13,12 @@ mod common;
|
|||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase15_shape_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 ruby_available() -> bool {
|
||||
std::process::Command::new("ruby")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
|
|
@ -62,9 +54,21 @@ mod phase15_shape_tests {
|
|||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Ruby, "ruby", shape, file, func, cap, sink_line, kind, slot,
|
||||
) -> Option<VerifyResult> {
|
||||
// Phase 29 (Track I): structured prerequisite gating replaces
|
||||
// the bespoke `ruby_available()` + per-test
|
||||
// `eprintln!("SKIP ..."); return;` pattern.
|
||||
run_shape_fixture_lang_or_skip(
|
||||
&[Prerequisite::CommandAvailable("ruby")],
|
||||
Lang::Ruby,
|
||||
"ruby",
|
||||
shape,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
kind,
|
||||
slot,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -72,27 +76,23 @@ mod phase15_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn sinatra_route_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"sinatra_route", "vuln.rb", "run", Cap::CODE_EXEC, 7,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("sinatra_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sinatra_route_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"sinatra_route", "benign.rb", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("sinatra_route", &r);
|
||||
}
|
||||
|
||||
|
|
@ -100,27 +100,23 @@ mod phase15_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn rails_action_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"rails_action", "vuln.rb", "index", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("rails_action", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rails_action_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"rails_action", "benign.rb", "index", Cap::CODE_EXEC, 20,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("rails_action", &r);
|
||||
}
|
||||
|
||||
|
|
@ -128,27 +124,23 @@ mod phase15_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn rack_middleware_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"rack_middleware", "vuln.rb", "call", Cap::CODE_EXEC, 9,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("rack_middleware", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rack_middleware_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"rack_middleware", "benign.rb", "call", Cap::CODE_EXEC, 11,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("rack_middleware", &r);
|
||||
}
|
||||
|
||||
|
|
@ -156,27 +148,23 @@ mod phase15_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn controller_method_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"controller_method", "vuln.rb", "authenticate", Cap::CODE_EXEC, 7,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("controller_method", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_method_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"controller_method", "benign.rb", "authenticate", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("controller_method", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,20 +290,12 @@ mod rust_fixture_tests {
|
|||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase16_shape_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 rust_available() -> bool {
|
||||
std::process::Command::new("cargo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
|
|
@ -339,9 +331,24 @@ mod phase16_shape_tests {
|
|||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Rust, "rust", shape, file, func, cap, sink_line, kind, slot,
|
||||
) -> Option<VerifyResult> {
|
||||
// Phase 29 (Track I): replace the bespoke `rust_available()` +
|
||||
// per-test `eprintln!("SKIP ..."); return;` blocks with the
|
||||
// structured `Prerequisite::CommandAvailable("cargo")` gate.
|
||||
// The helper emits the same SKIP line and returns `None` so
|
||||
// each test can short-circuit via `let Some(r) = run(...) else
|
||||
// { return; };`.
|
||||
run_shape_fixture_lang_or_skip(
|
||||
&[Prerequisite::CommandAvailable("cargo")],
|
||||
Lang::Rust,
|
||||
"rust",
|
||||
shape,
|
||||
file,
|
||||
func,
|
||||
cap,
|
||||
sink_line,
|
||||
kind,
|
||||
slot,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -349,27 +356,23 @@ mod phase16_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn actix_route_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"actix_route", "vuln.rs", "handler", Cap::CODE_EXEC, 16,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actix_route_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"actix_route", "benign.rs", "handler", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
|
|
@ -377,27 +380,23 @@ mod phase16_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn axum_handler_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"axum_handler", "vuln.rs", "handler", Cap::CODE_EXEC, 15,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axum_handler_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"axum_handler", "benign.rs", "handler", Cap::CODE_EXEC, 13,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
|
|
@ -405,27 +404,23 @@ mod phase16_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn clap_cli_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"clap_cli", "vuln.rs", "run", Cap::CODE_EXEC, 17,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_cli_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"clap_cli", "benign.rs", "run", Cap::CODE_EXEC, 13,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
|
|
@ -433,27 +428,23 @@ mod phase16_shape_tests {
|
|||
|
||||
#[test]
|
||||
fn libfuzzer_target_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"libfuzzer_target", "vuln.rs", "fuzz_target", Cap::CODE_EXEC, 15,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_target_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
let Some(r) = run(
|
||||
"libfuzzer_target", "benign.rs", "fuzz_target", Cap::CODE_EXEC, 13,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
assert_not_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,6 +441,220 @@ fn python_http_shim_recorder_is_noop_without_log_env() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: Node 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.
|
||||
if !node_available() {
|
||||
eprintln!("SKIP: node 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("node/http/vuln/main.js")).expect("read fixture");
|
||||
let mut combined = String::with_capacity(node_probe_shim().len() + fixture.len() + 64);
|
||||
combined.push_str(node_probe_shim());
|
||||
combined.push_str("\n// ── fixture begins ─\n");
|
||||
combined.push_str(&fixture);
|
||||
|
||||
let script_path = workdir.path().join("driver_http.js");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("node 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 Node 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("node:http"),
|
||||
"kwargs passed to __nyx_stub_http_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !node_available() {
|
||||
eprintln!("SKIP: node 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("node/http/vuln/main.js")).expect("read fixture");
|
||||
let mut combined = String::new();
|
||||
combined.push_str(node_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(&fixture);
|
||||
let script_path = workdir.path().join("driver_http_no_log.js");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_HTTP_LOG")
|
||||
.output()
|
||||
.expect("node 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 php_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: PHP 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.
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php 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("php/http/vuln/main.php")).expect("read fixture");
|
||||
let body = strip_php_open_tag(&fixture);
|
||||
let mut combined = String::with_capacity(php_probe_shim().len() + body.len() + 64);
|
||||
combined.push_str("<?php\n");
|
||||
combined.push_str(php_probe_shim());
|
||||
combined.push_str("\n// ── fixture begins ─\n");
|
||||
combined.push_str(body);
|
||||
|
||||
let script_path = workdir.path().join("driver_http.php");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("php")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("php 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 PHP 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("curl"),
|
||||
"kwargs passed to __nyx_stub_http_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php 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("php/http/vuln/main.php")).expect("read fixture");
|
||||
let body = strip_php_open_tag(&fixture);
|
||||
let mut combined = String::new();
|
||||
combined.push_str("<?php\n");
|
||||
combined.push_str(php_probe_shim());
|
||||
combined.push('\n');
|
||||
combined.push_str(body);
|
||||
let script_path = workdir.path().join("driver_http_no_log.php");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("php")
|
||||
.arg(&script_path)
|
||||
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_HTTP_LOG")
|
||||
.output()
|
||||
.expect("php 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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue