mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss/grind] deferred session-0018 (20260516T052512Z-20f8)
This commit is contained in:
parent
1062846a07
commit
cf2dfb0fcf
4 changed files with 287 additions and 102 deletions
|
|
@ -240,7 +240,7 @@ impl JavaShape {
|
||||||
/// dependencies; matches the
|
/// dependencies; matches the
|
||||||
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
||||||
pub fn probe_shim() -> &'static str {
|
pub fn probe_shim() -> &'static str {
|
||||||
r#"
|
r##"
|
||||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──
|
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──
|
||||||
private static final String[] __NYX_DENY = {
|
private static final String[] __NYX_DENY = {
|
||||||
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
"TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY",
|
||||||
|
|
@ -371,7 +371,40 @@ pub fn probe_shim() -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"#
|
|
||||||
|
// 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 / Ruby siblings so the host-side HttpStub
|
||||||
|
// log-line merger parses all six 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. The hash prefix is emitted via
|
||||||
|
// String.valueOf('#') so this method body contains no literal hash-after-
|
||||||
|
// double-quote sequence that would terminate the surrounding Rust raw
|
||||||
|
// string.
|
||||||
|
static void __nyx_stub_http_record(String method, String url, String body, java.util.Map<String,String> detail) {
|
||||||
|
String p = System.getenv("NYX_HTTP_LOG");
|
||||||
|
if (p == null || p.isEmpty()) return;
|
||||||
|
String hashSp = String.valueOf('#') + " ";
|
||||||
|
try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) {
|
||||||
|
fw.write(hashSp + "method: " + method + "\n");
|
||||||
|
fw.write(hashSp + "url: " + url + "\n");
|
||||||
|
if (body != null) {
|
||||||
|
fw.write(hashSp + "body: " + body + "\n");
|
||||||
|
}
|
||||||
|
if (detail != null) {
|
||||||
|
for (java.util.Map.Entry<String,String> e : detail.entrySet()) {
|
||||||
|
fw.write(hashSp + e.getKey() + ": " + e.getValue() + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fw.write(method + " " + url + "\n");
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Runtime / pom.xml synthesis (Phase 09) ──────────────────────────────────
|
// ── Runtime / pom.xml synthesis (Phase 09) ──────────────────────────────────
|
||||||
|
|
@ -1040,6 +1073,27 @@ mod tests {
|
||||||
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn probe_shim_publishes_stub_http_recorder() {
|
||||||
|
let shim = probe_shim();
|
||||||
|
assert!(
|
||||||
|
shim.contains("static void __nyx_stub_http_record"),
|
||||||
|
"Java probe shim must define __nyx_stub_http_record"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("\"NYX_HTTP_LOG\""),
|
||||||
|
"Java HTTP recorder must read NYX_HTTP_LOG to find the side-channel log"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("\"method: \""),
|
||||||
|
"Java HTTP recorder must emit a method detail line"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("\"url: \""),
|
||||||
|
"Java HTTP recorder must emit a url detail line"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||||
let step = chain_step(Some(b"<prev>"));
|
let step = chain_step(Some(b"<prev>"));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -463,25 +463,12 @@ mod java_fixture_tests {
|
||||||
|
|
||||||
#[cfg(feature = "dynamic")]
|
#[cfg(feature = "dynamic")]
|
||||||
mod phase14_shape_tests {
|
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::dynamic::spec::PayloadSlot;
|
||||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||||
use nyx_scanner::labels::Cap;
|
use nyx_scanner::labels::Cap;
|
||||||
use nyx_scanner::symbol::Lang;
|
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) {
|
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.status,
|
result.status,
|
||||||
|
|
@ -517,8 +504,18 @@ mod phase14_shape_tests {
|
||||||
sink_line: u32,
|
sink_line: u32,
|
||||||
kind: EntryKind,
|
kind: EntryKind,
|
||||||
slot: PayloadSlot,
|
slot: PayloadSlot,
|
||||||
) -> VerifyResult {
|
) -> Option<VerifyResult> {
|
||||||
run_shape_fixture_lang(
|
// 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,
|
Lang::Java, "java", shape, file, func, cap, sink_line, kind, slot,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -527,27 +524,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_method_vuln_is_confirmed() {
|
fn static_method_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"static_method", "Vuln.java", "processInput", Cap::CODE_EXEC, 12,
|
"static_method", "Vuln.java", "processInput", Cap::CODE_EXEC, 12,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("static_method", &r);
|
assert_confirmed("static_method", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_method_benign_not_confirmed() {
|
fn static_method_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"static_method", "Benign.java", "processInput", Cap::CODE_EXEC, 13,
|
"static_method", "Benign.java", "processInput", Cap::CODE_EXEC, 13,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("static_method", &r);
|
assert_not_confirmed("static_method", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,27 +548,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_main_vuln_is_confirmed() {
|
fn static_main_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"static_main", "Vuln.java", "main", Cap::CODE_EXEC, 13,
|
"static_main", "Vuln.java", "main", Cap::CODE_EXEC, 13,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("static_main", &r);
|
assert_confirmed("static_main", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_main_benign_not_confirmed() {
|
fn static_main_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"static_main", "Benign.java", "main", Cap::CODE_EXEC, 12,
|
"static_main", "Benign.java", "main", Cap::CODE_EXEC, 12,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("static_main", &r);
|
assert_not_confirmed("static_main", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,27 +572,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn servlet_doget_vuln_is_confirmed() {
|
fn servlet_doget_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"servlet_doget", "Vuln.java", "doGet", Cap::CODE_EXEC, 14,
|
"servlet_doget", "Vuln.java", "doGet", Cap::CODE_EXEC, 14,
|
||||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("servlet_doget", &r);
|
assert_confirmed("servlet_doget", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn servlet_doget_benign_not_confirmed() {
|
fn servlet_doget_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"servlet_doget", "Benign.java", "doGet", Cap::CODE_EXEC, 14,
|
"servlet_doget", "Benign.java", "doGet", Cap::CODE_EXEC, 14,
|
||||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("servlet_doget", &r);
|
assert_not_confirmed("servlet_doget", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -611,27 +596,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn servlet_dopost_vuln_is_confirmed() {
|
fn servlet_dopost_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"servlet_dopost", "Vuln.java", "doPost", Cap::CODE_EXEC, 13,
|
"servlet_dopost", "Vuln.java", "doPost", Cap::CODE_EXEC, 13,
|
||||||
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("servlet_dopost", &r);
|
assert_confirmed("servlet_dopost", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn servlet_dopost_benign_not_confirmed() {
|
fn servlet_dopost_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"servlet_dopost", "Benign.java", "doPost", Cap::CODE_EXEC, 12,
|
"servlet_dopost", "Benign.java", "doPost", Cap::CODE_EXEC, 12,
|
||||||
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("servlet_dopost", &r);
|
assert_not_confirmed("servlet_dopost", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -639,27 +620,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spring_controller_vuln_is_confirmed() {
|
fn spring_controller_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"spring_controller", "Vuln.java", "run", Cap::CODE_EXEC, 16,
|
"spring_controller", "Vuln.java", "run", Cap::CODE_EXEC, 16,
|
||||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("spring_controller", &r);
|
assert_confirmed("spring_controller", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spring_controller_benign_not_confirmed() {
|
fn spring_controller_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"spring_controller", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
"spring_controller", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
||||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("spring_controller", &r);
|
assert_not_confirmed("spring_controller", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -667,27 +644,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn junit_test_vuln_is_confirmed() {
|
fn junit_test_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"junit_test", "Vuln.java", "testRun", Cap::CODE_EXEC, 17,
|
"junit_test", "Vuln.java", "testRun", Cap::CODE_EXEC, 17,
|
||||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("junit_test", &r);
|
assert_confirmed("junit_test", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn junit_test_benign_not_confirmed() {
|
fn junit_test_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"junit_test", "Benign.java", "testRun", Cap::CODE_EXEC, 15,
|
"junit_test", "Benign.java", "testRun", Cap::CODE_EXEC, 15,
|
||||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("junit_test", &r);
|
assert_not_confirmed("junit_test", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -695,27 +668,23 @@ mod phase14_shape_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quarkus_route_vuln_is_confirmed() {
|
fn quarkus_route_vuln_is_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"quarkus_route", "Vuln.java", "run", Cap::CODE_EXEC, 17,
|
"quarkus_route", "Vuln.java", "run", Cap::CODE_EXEC, 17,
|
||||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_confirmed("quarkus_route", &r);
|
assert_confirmed("quarkus_route", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quarkus_route_benign_not_confirmed() {
|
fn quarkus_route_benign_not_confirmed() {
|
||||||
if !java_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: javac/java not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"quarkus_route", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
"quarkus_route", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
||||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||||
);
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
assert_not_confirmed("quarkus_route", &r);
|
assert_not_confirmed("quarkus_route", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#![cfg(feature = "dynamic")]
|
#![cfg(feature = "dynamic")]
|
||||||
|
|
||||||
use nyx_scanner::dynamic::lang::go::probe_shim as go_probe_shim;
|
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::javascript::probe_shim as node_probe_shim;
|
||||||
use nyx_scanner::dynamic::lang::php::probe_shim as php_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::python::probe_shim as python_probe_shim;
|
||||||
|
|
@ -70,6 +71,37 @@ fn ruby_available() -> bool {
|
||||||
.unwrap_or(false)
|
.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`
|
/// Wrap the body-only Go HTTP fixture in a complete `package main`
|
||||||
/// program: stdlib imports needed by the spliced probe shim plus the
|
/// program: stdlib imports needed by the spliced probe shim plus the
|
||||||
/// fragment's own `fmt` / `os` references, the shim itself, and 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]
|
#[test]
|
||||||
fn node_sql_shim_recorder_is_noop_without_log_env() {
|
fn node_sql_shim_recorder_is_noop_without_log_env() {
|
||||||
if !node_available() {
|
if !node_available() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue