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

This commit is contained in:
pitboss 2026-05-16 10:21:33 -05:00
parent 1062846a07
commit cf2dfb0fcf
4 changed files with 287 additions and 102 deletions

View file

@ -0,0 +1,24 @@
// Phase 10 (Track D.3) stub-end-to-end fixture: Java + 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 file is a body-only fragment: the companion test in
// tests/stubs_e2e_per_lang.rs wraps it with a `public class Main { … }`
// shell that splices the Java probe shim as class members ahead of
// `public static void main`, so the shim's __nyx_stub_http_record helper
// is in scope without needing an import. java.net.HttpURLConnection is
// JDK stdlib, so no extra classpath dep is required.
String method = "GET";
String url = "http://169.254.169.254/latest/meta-data/";
String body = "";
java.util.Map<String,String> detail = new java.util.LinkedHashMap<>();
detail.put("driver", "HttpURLConnection");
__nyx_stub_http_record(method, url, body, detail);
String ep = System.getenv("NYX_HTTP_ENDPOINT");
System.out.println(ep == null ? "no-endpoint" : ep);

View file

@ -463,25 +463,12 @@ mod java_fixture_tests {
#[cfg(feature = "dynamic")]
mod phase14_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 java_available() -> bool {
std::process::Command::new("javac")
.arg("-version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
&& std::process::Command::new("java")
.arg("-version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn assert_confirmed(shape: &str, result: &VerifyResult) {
assert_eq!(
result.status,
@ -517,8 +504,18 @@ mod phase14_shape_tests {
sink_line: u32,
kind: EntryKind,
slot: PayloadSlot,
) -> VerifyResult {
run_shape_fixture_lang(
) -> Option<VerifyResult> {
// Phase 29 (Track I): replace the bespoke `java_available()` +
// per-test `eprintln!("SKIP ..."); return;` blocks with the
// structured `Prerequisite::CommandAvailable("javac"|"java")`
// 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("javac"),
Prerequisite::CommandAvailable("java"),
],
Lang::Java, "java", shape, file, func, cap, sink_line, kind, slot,
)
}
@ -527,27 +524,23 @@ mod phase14_shape_tests {
#[test]
fn static_method_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"static_method", "Vuln.java", "processInput", Cap::CODE_EXEC, 12,
EntryKind::Function, PayloadSlot::Param(0),
);
) else {
return;
};
assert_confirmed("static_method", &r);
}
#[test]
fn static_method_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"static_method", "Benign.java", "processInput", Cap::CODE_EXEC, 13,
EntryKind::Function, PayloadSlot::Param(0),
);
) else {
return;
};
assert_not_confirmed("static_method", &r);
}
@ -555,27 +548,23 @@ mod phase14_shape_tests {
#[test]
fn static_main_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"static_main", "Vuln.java", "main", Cap::CODE_EXEC, 13,
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
);
) else {
return;
};
assert_confirmed("static_main", &r);
}
#[test]
fn static_main_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"static_main", "Benign.java", "main", Cap::CODE_EXEC, 12,
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
);
) else {
return;
};
assert_not_confirmed("static_main", &r);
}
@ -583,27 +572,23 @@ mod phase14_shape_tests {
#[test]
fn servlet_doget_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"servlet_doget", "Vuln.java", "doGet", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
);
) else {
return;
};
assert_confirmed("servlet_doget", &r);
}
#[test]
fn servlet_doget_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"servlet_doget", "Benign.java", "doGet", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
);
) else {
return;
};
assert_not_confirmed("servlet_doget", &r);
}
@ -611,27 +596,23 @@ mod phase14_shape_tests {
#[test]
fn servlet_dopost_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"servlet_dopost", "Vuln.java", "doPost", Cap::CODE_EXEC, 13,
EntryKind::HttpRoute, PayloadSlot::HttpBody,
);
) else {
return;
};
assert_confirmed("servlet_dopost", &r);
}
#[test]
fn servlet_dopost_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"servlet_dopost", "Benign.java", "doPost", Cap::CODE_EXEC, 12,
EntryKind::HttpRoute, PayloadSlot::HttpBody,
);
) else {
return;
};
assert_not_confirmed("servlet_dopost", &r);
}
@ -639,27 +620,23 @@ mod phase14_shape_tests {
#[test]
fn spring_controller_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"spring_controller", "Vuln.java", "run", Cap::CODE_EXEC, 16,
EntryKind::HttpRoute, PayloadSlot::Param(0),
);
) else {
return;
};
assert_confirmed("spring_controller", &r);
}
#[test]
fn spring_controller_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"spring_controller", "Benign.java", "run", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::Param(0),
);
) else {
return;
};
assert_not_confirmed("spring_controller", &r);
}
@ -667,27 +644,23 @@ mod phase14_shape_tests {
#[test]
fn junit_test_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"junit_test", "Vuln.java", "testRun", Cap::CODE_EXEC, 17,
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
);
) else {
return;
};
assert_confirmed("junit_test", &r);
}
#[test]
fn junit_test_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"junit_test", "Benign.java", "testRun", Cap::CODE_EXEC, 15,
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
);
) else {
return;
};
assert_not_confirmed("junit_test", &r);
}
@ -695,27 +668,23 @@ mod phase14_shape_tests {
#[test]
fn quarkus_route_vuln_is_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"quarkus_route", "Vuln.java", "run", Cap::CODE_EXEC, 17,
EntryKind::HttpRoute, PayloadSlot::Param(0),
);
) else {
return;
};
assert_confirmed("quarkus_route", &r);
}
#[test]
fn quarkus_route_benign_not_confirmed() {
if !java_available() {
eprintln!("SKIP: javac/java not available");
return;
}
let r = run(
let Some(r) = run(
"quarkus_route", "Benign.java", "run", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::Param(0),
);
) else {
return;
};
assert_not_confirmed("quarkus_route", &r);
}

View file

@ -21,6 +21,7 @@
#![cfg(feature = "dynamic")]
use nyx_scanner::dynamic::lang::go::probe_shim as go_probe_shim;
use nyx_scanner::dynamic::lang::java::probe_shim as java_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;
@ -70,6 +71,37 @@ fn ruby_available() -> bool {
.unwrap_or(false)
}
fn java_available() -> bool {
// The Java shim helpers use `java MainSource.java` single-file
// source-mode (JEP 330, JDK 11+) so only the `java` runtime is
// strictly required. An older `java` binary that does not support
// source-mode is treated as missing and the test eprintln-skips.
Command::new("java")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Wrap the body-only Java HTTP fixture in a complete `public class Main`
/// source: splice the Java probe shim as class members ahead of
/// `public static void main`, then put the fragment in the method body.
/// Mirrors the production [`JavaEmitter::emit`] ordering — the shim is
/// declared first so any sink rewrite in the body has the shim helpers
/// in scope. The throws clause lets the fragment use checked-exception
/// stdlib calls without per-line try/catch.
fn wrap_java_fragment(body: &str, shim: &str) -> String {
format!(
"public class Main {{\n\
{shim}\n\
\n\
public static void main(String[] args) throws Exception {{\n\
{body}\n\
}}\n\
}}\n"
)
}
/// 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
@ -909,6 +941,112 @@ fn ruby_http_shim_recorder_is_noop_without_log_env() {
);
}
#[test]
fn java_http_stub_captures_attempted_outbound_via_shim_recorder() {
// Phase 10 (Track D.3) HTTP recording: Java leg of the side-channel
// `__nyx_stub_http_record` helper. Mirrors the Python / Node / PHP /
// Go / Ruby HTTP tests — records an SSRF attempt without issuing the
// actual network call. Uses `java MainSource.java` single-file
// source-mode (JEP 330, JDK 11+) so no separate `javac` step is
// required.
if !java_available() {
eprintln!("SKIP: java 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 fragment = std::fs::read_to_string(fixture_path("java/http/vuln/main.java.fragment"))
.expect("read java fragment");
let combined = wrap_java_fragment(&fragment, java_probe_shim());
// Single-file source-mode requires the filename to match the public
// class — name the file `Main.java` so `java Main.java` compiles
// and runs in one step.
let script_path = workdir.path().join("Main.java");
std::fs::write(&script_path, combined).expect("write java driver");
let output = Command::new("java")
.arg(&script_path)
.env("NYX_HTTP_ENDPOINT", &endpoint)
.env(recording.0, &recording.1)
.output()
.expect("java 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 Java 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("HttpURLConnection"),
"detail map entries passed to __nyx_stub_http_record must surface as event detail entries"
);
}
#[test]
fn java_http_shim_recorder_is_noop_without_log_env() {
if !java_available() {
eprintln!("SKIP: java not available");
return;
}
let workdir = TempDir::new().expect("tempdir");
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
let endpoint = stub.endpoint();
let fragment = std::fs::read_to_string(fixture_path("java/http/vuln/main.java.fragment"))
.expect("read java fragment");
let combined = wrap_java_fragment(&fragment, java_probe_shim());
let script_path = workdir.path().join("Main.java");
std::fs::write(&script_path, combined).expect("write java driver");
let output = Command::new("java")
.arg(&script_path)
.env("NYX_HTTP_ENDPOINT", &endpoint)
.env_remove("NYX_HTTP_LOG")
.output()
.expect("java 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() {