[pitboss/grind] deferred session-0015 (20260516T052512Z-20f8)

This commit is contained in:
pitboss 2026-05-16 08:53:23 -05:00
parent 6a169f51b8
commit f701b43152
6 changed files with 459 additions and 120 deletions

View file

@ -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

View 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();

View 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();

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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() {