mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0020 (20260516T052512Z-20f8)
This commit is contained in:
parent
04b3d88eb4
commit
aa209148b0
8 changed files with 649 additions and 13 deletions
|
|
@ -433,6 +433,34 @@ func __nyx_stub_http_record(method, url, body string, detail map[string]string)
|
|||
}
|
||||
f.WriteString(method + " " + url + "\n")
|
||||
}
|
||||
|
||||
// Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
|
||||
// SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
|
||||
// sink callsite whose query never reaches the on-the-wire SQLite engine
|
||||
// (no database/sql driver imported, query pre-flighted before sql.Open,
|
||||
// network-isolated sandbox) can call this helper to surface the attempted
|
||||
// query. Hash-prefixed detail lines followed by the query line so
|
||||
// SqlStub::drain_events parses every language stream identically. No-op
|
||||
// when NYX_SQL_LOG is unset so the same harness still runs cleanly under
|
||||
// modes that did not spawn a stub.
|
||||
func __nyx_stub_sql_record(query string, detail map[string]string) {
|
||||
p := os.Getenv("NYX_SQL_LOG")
|
||||
if p == "" {
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
for k, v := range detail {
|
||||
f.WriteString("# " + k + ": " + v + "\n")
|
||||
}
|
||||
f.WriteString(query)
|
||||
if !strings.HasSuffix(query, "\n") {
|
||||
f.WriteString("\n")
|
||||
}
|
||||
}
|
||||
"##
|
||||
}
|
||||
|
||||
|
|
@ -921,6 +949,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("func __nyx_stub_sql_record"),
|
||||
"Go probe shim must define __nyx_stub_sql_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("NYX_SQL_LOG"),
|
||||
"stub recorder must read NYX_SQL_LOG"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("strings.HasSuffix(query, \"\\n\")"),
|
||||
"Go SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
|
|
|
|||
|
|
@ -404,6 +404,35 @@ pub fn probe_shim() -> &'static str {
|
|||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
|
||||
// SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
|
||||
// sink call site whose query never reaches the on-the-wire SQLite engine
|
||||
// (e.g. classpath lacks sqlite-jdbc, or the harness pre-flights the SQL
|
||||
// string before opening the connection) can call this helper to surface
|
||||
// the attempted query. Hash-prefixed detail lines followed by the query
|
||||
// line so SqlStub::drain_events parses every language stream identically.
|
||||
// Same hash-via-String.valueOf trick as __nyx_stub_http_record so this
|
||||
// method body contains no literal `"#` sequence that would terminate the
|
||||
// surrounding Rust raw string.
|
||||
static void __nyx_stub_sql_record(String query, java.util.Map<String,String> detail) {
|
||||
String p = System.getenv("NYX_SQL_LOG");
|
||||
if (p == null || p.isEmpty()) return;
|
||||
String hashSp = String.valueOf('#') + " ";
|
||||
try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) {
|
||||
if (detail != null) {
|
||||
for (java.util.Map.Entry<String,String> e : detail.entrySet()) {
|
||||
fw.write(hashSp + e.getKey() + ": " + e.getValue() + "\n");
|
||||
}
|
||||
}
|
||||
fw.write(query);
|
||||
if (!query.endsWith("\n")) {
|
||||
fw.write("\n");
|
||||
}
|
||||
} catch (java.io.IOException e) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
"##
|
||||
}
|
||||
|
||||
|
|
@ -1094,6 +1123,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("static void __nyx_stub_sql_record"),
|
||||
"Java probe shim must define __nyx_stub_sql_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("\"NYX_SQL_LOG\""),
|
||||
"Java SQL recorder must read NYX_SQL_LOG to find the side-channel log"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("query.endsWith(\"\\n\")"),
|
||||
"Java SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
|
|
|
|||
|
|
@ -305,6 +305,29 @@ def __nyx_stub_http_record(method, url, body = nil, **detail)
|
|||
rescue StandardError
|
||||
end
|
||||
end
|
||||
|
||||
# Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
|
||||
# SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
|
||||
# sink call site whose query never reaches the on-the-wire SQLite engine
|
||||
# (no sqlite3 gem on the host, query pre-flighted before
|
||||
# SQLite3::Database.open) can call this helper to surface the attempted
|
||||
# query. Hash-prefixed detail lines followed by the query line so
|
||||
# SqlStub::drain_events parses every language stream identically. No-op
|
||||
# when NYX_SQL_LOG is unset. Single-quoted Ruby string literals keep this
|
||||
# helper free of the literal hash-after-double-quote sequence.
|
||||
def __nyx_stub_sql_record(query, **detail)
|
||||
p = ENV['NYX_SQL_LOG']
|
||||
return if p.nil? || p.empty?
|
||||
begin
|
||||
File.open(p, 'a') do |f|
|
||||
detail.each { |k, v| f.puts('# ' + k.to_s + ': ' + v.to_s) }
|
||||
line = query.to_s
|
||||
line += "\n" unless line.end_with?("\n")
|
||||
f.write(line)
|
||||
end
|
||||
rescue StandardError
|
||||
end
|
||||
end
|
||||
"#
|
||||
}
|
||||
|
||||
|
|
@ -825,6 +848,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_recorder() {
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("def __nyx_stub_sql_record"),
|
||||
"Ruby probe shim must define __nyx_stub_sql_record"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("ENV['NYX_SQL_LOG']"),
|
||||
"Ruby SQL recorder must read NYX_SQL_LOG to find the side-channel log"
|
||||
);
|
||||
assert!(
|
||||
shim.contains("line.end_with?"),
|
||||
"Ruby SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_step_splices_probe_shim_for_composite_reverify() {
|
||||
let step = chain_step(Some(b"<prev>"));
|
||||
|
|
|
|||
29
tests/dynamic_fixtures/stubs_e2e/go/sql/vuln/main.go
Normal file
29
tests/dynamic_fixtures/stubs_e2e/go/sql/vuln/main.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Phase 10 (Track D.3) stub-end-to-end fixture: Go + SQL.
|
||||
//
|
||||
// Body-only fragment, not a standalone `go run`-able program. The
|
||||
// companion test in `tests/stubs_e2e_per_lang.rs` wraps these lines
|
||||
// in `package main` + the union of stdlib imports required by both
|
||||
// the spliced probe shim and this fragment, places the Go probe
|
||||
// shim ahead of `func main`, and then invokes `go run` on the
|
||||
// resulting file.
|
||||
//
|
||||
// The verifier publishes:
|
||||
//
|
||||
// NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
|
||||
// NYX_SQL_LOG — companion log path the harness appends executed
|
||||
// queries to so the host SqlStub picks them up on
|
||||
// drain_events() even when the harness never opens
|
||||
// an on-the-wire driver (no go-sqlite3 / pgx /
|
||||
// mysql dep on the dynamic CI matrix; query
|
||||
// pre-flighted before sql.Open).
|
||||
//
|
||||
// This fragment records the tautology query through the Go shim
|
||||
// helper __nyx_stub_sql_record as `driver = "manual"` so the test
|
||||
// stays stdlib-only — no `database/sql` import, no go.mod driver
|
||||
// dep, no libsqlite3-dev system package. Mirrors the Phase 26
|
||||
// "no live driver available" path that real Go sink callsites take
|
||||
// when the build matrix lacks a driver.
|
||||
query := "SELECT 1 WHERE 'a' = 'a' OR 1=1 --"
|
||||
__nyx_stub_sql_record(query, map[string]string{"driver": "manual"})
|
||||
// Echo so the host can confirm the driver ran end-to-end.
|
||||
fmt.Print(os.Getenv("NYX_SQL_ENDPOINT"))
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 10 (Track D.3) stub-end-to-end fixture: Java + SQL.
|
||||
//
|
||||
// The verifier publishes:
|
||||
//
|
||||
// * NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
|
||||
// * NYX_SQL_LOG — companion log path the harness appends executed
|
||||
// queries to so the host SqlStub picks them up on drain_events()
|
||||
// even when the harness never opens an on-the-wire JDBC connection
|
||||
// (classpath lacks sqlite-jdbc, SQL string is pre-flighted before
|
||||
// DriverManager.getConnection, sandbox blocks file-DB access).
|
||||
//
|
||||
// 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_sql_record helper
|
||||
// is in scope. The fixture stays JDK-stdlib only — no java.sql import,
|
||||
// no sqlite-jdbc jar on the classpath — by recording the attempted
|
||||
// tautology with `driver = "manual"`. This mirrors the Phase 26
|
||||
// "no live driver available" path that real Java sink callsites take
|
||||
// when the build matrix lacks a JDBC driver.
|
||||
String query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
java.util.Map<String,String> detail = new java.util.LinkedHashMap<>();
|
||||
detail.put("driver", "manual");
|
||||
__nyx_stub_sql_record(query, detail);
|
||||
String ep = System.getenv("NYX_SQL_ENDPOINT");
|
||||
System.out.println(ep == null ? "no-endpoint" : ep);
|
||||
21
tests/dynamic_fixtures/stubs_e2e/ruby/sql/vuln/main.rb
Normal file
21
tests/dynamic_fixtures/stubs_e2e/ruby/sql/vuln/main.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Phase 10 (Track D.3) stub-end-to-end fixture: Ruby + SQL.
|
||||
#
|
||||
# The verifier publishes:
|
||||
#
|
||||
# * NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
|
||||
# * NYX_SQL_LOG — companion log path the harness appends executed
|
||||
# queries to so the host SqlStub picks them up on drain_events()
|
||||
# even when the harness never opens an on-the-wire driver (sqlite3
|
||||
# gem absent on minimal CI images, query pre-flighted before
|
||||
# SQLite3::Database.open).
|
||||
#
|
||||
# This fixture stays gem-free by recording the tautology through
|
||||
# __nyx_stub_sql_record as driver = 'manual'. No sqlite3 require, no
|
||||
# Gemfile dep, no Prerequisite::GemAvailable variant required. Mirrors
|
||||
# the Phase 26 "no live driver available" path that real Ruby sink
|
||||
# callsites take when the build matrix lacks a driver.
|
||||
|
||||
query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --"
|
||||
__nyx_stub_sql_record(query, driver: 'manual')
|
||||
# Echo so the host can confirm the driver ran end-to-end.
|
||||
$stdout.puts(ENV['NYX_SQL_ENDPOINT'] || 'no-endpoint')
|
||||
18
tests/dynamic_fixtures/stubs_e2e/rust/sql/vuln/main.rs
Normal file
18
tests/dynamic_fixtures/stubs_e2e/rust/sql/vuln/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 10 (Track D.3) — Rust SQL recorder body-only fragment.
|
||||
//
|
||||
// Wrapped at test time by `wrap_rust_fragment(body, shim)` in
|
||||
// `tests/stubs_e2e_per_lang.rs`: the wrapper prepends the Rust probe
|
||||
// shim (which carries `__nyx_stub_sql_record`) and a one-line
|
||||
// `Cargo.toml` so `cargo run --quiet` builds the program in place.
|
||||
//
|
||||
// Rust has no stdlib SQLite client (rusqlite is a heavyweight C-link
|
||||
// dep that would force a libsqlite3-dev prereq on the dynamic CI
|
||||
// matrix). The fixture surfaces the attempted tautology query
|
||||
// through the shim recorder so the host-side SqlStub captures it as
|
||||
// `driver = "manual"`, mirroring the Phase 26 "no live driver
|
||||
// available" path that real Rust sink callsites take when the build
|
||||
// matrix lacks a DB driver.
|
||||
let _endpoint = std::env::var("NYX_SQL_ENDPOINT").unwrap_or_default();
|
||||
let query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
let detail: &[(&str, &str)] = &[("driver", "manual")];
|
||||
__nyx_stub_sql_record(query, detail);
|
||||
|
|
@ -153,21 +153,29 @@ fn wrap_rust_fragment(body: &str, shim: &str) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// One-line Cargo.toml for the Rust stub-recorder driver. Mirrors
|
||||
/// Per-fixture Cargo.toml for the Rust stub-recorder driver. Mirrors
|
||||
/// the Phase 26 chain_step manifest (session 0014) — `[[bin]]` points
|
||||
/// at `main.rs` so `cargo run --quiet` builds the source the test
|
||||
/// just wrote, and `libc = "0.2"` is unconditionally pinned because
|
||||
/// the spliced probe shim's `__nyx_install_crash_guard` references
|
||||
/// `libc::sigaction` on Unix.
|
||||
const RUST_STUB_CARGO_TOML: &str = "[package]\n\
|
||||
name = \"nyx-stub-driver\"\n\
|
||||
version = \"0.0.1\"\n\
|
||||
edition = \"2021\"\n\n\
|
||||
[[bin]]\n\
|
||||
name = \"stub_driver\"\n\
|
||||
path = \"main.rs\"\n\n\
|
||||
[dependencies]\n\
|
||||
libc = \"0.2\"\n";
|
||||
/// `libc::sigaction` on Unix. Caller supplies a unique `slug` per
|
||||
/// test so the package + binary names do not collide in the shared
|
||||
/// `CARGO_TARGET_DIR` when nextest runs the Rust stub tests in
|
||||
/// parallel (every test still benefits from the cached `libc` build,
|
||||
/// only the final `nyx-stub-driver-<slug>` link is per-test).
|
||||
fn rust_stub_cargo_toml(slug: &str) -> String {
|
||||
format!(
|
||||
"[package]\n\
|
||||
name = \"nyx-stub-driver-{slug}\"\n\
|
||||
version = \"0.0.1\"\n\
|
||||
edition = \"2021\"\n\n\
|
||||
[[bin]]\n\
|
||||
name = \"stub_driver_{slug}\"\n\
|
||||
path = \"main.rs\"\n\n\
|
||||
[dependencies]\n\
|
||||
libc = \"0.2\"\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn fixture_path(rel: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
|
|
@ -876,6 +884,103 @@ fn go_http_shim_recorder_is_noop_without_log_env() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) SQL recording: Go leg of the side-channel
|
||||
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
|
||||
// Rust / Java SQL tests — the Go fragment never opens a live
|
||||
// `database/sql` handle (no driver imported; pulling go-sqlite3 /
|
||||
// pgx / mysql would force a go.mod dep onto every dynamic CI matrix
|
||||
// row) so it surfaces the attempted tautology query through the
|
||||
// shim recorder as `driver = "manual"`.
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fragment =
|
||||
std::fs::read_to_string(fixture_path("go/sql/vuln/main.go")).expect("read go fragment");
|
||||
let combined = wrap_go_fragment(&fragment, go_probe_shim());
|
||||
|
||||
let script_path = workdir.path().join("driver_sql.go");
|
||||
std::fs::write(&script_path, combined).expect("write go driver");
|
||||
|
||||
let output = Command::new("go")
|
||||
.arg("run")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("go 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(),
|
||||
"SqlStub must capture at least one event after the Go shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"detail map entries passed to __nyx_stub_sql_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment =
|
||||
std::fs::read_to_string(fixture_path("go/sql/vuln/main.go")).expect("read go fragment");
|
||||
let combined = wrap_go_fragment(&fragment, go_probe_shim());
|
||||
|
||||
let script_path = workdir.path().join("driver_sql_no_log.go");
|
||||
std::fs::write(&script_path, combined).expect("write go driver");
|
||||
|
||||
let output = Command::new("go")
|
||||
.arg("run")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("go driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_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 ruby_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: Ruby leg of the side-channel
|
||||
|
|
@ -983,6 +1088,105 @@ fn ruby_http_shim_recorder_is_noop_without_log_env() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) SQL recording: Ruby leg of the side-channel
|
||||
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
|
||||
// Rust / Java / Go SQL tests — the Ruby fragment never opens a live
|
||||
// sqlite3 handle (no require, no gem dep) so it surfaces the
|
||||
// attempted tautology query through the shim recorder as
|
||||
// `driver = "manual"`.
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("ruby/sql/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_sql.rb");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("ruby")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_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(),
|
||||
"SqlStub must capture at least one event after the Ruby shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"kwargs passed to __nyx_stub_sql_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fixture =
|
||||
std::fs::read_to_string(fixture_path("ruby/sql/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_sql_no_log.rb");
|
||||
std::fs::write(&script_path, combined).expect("write driver");
|
||||
|
||||
let output = Command::new("ruby")
|
||||
.arg(&script_path)
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("ruby driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_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 java_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) HTTP recording: Java leg of the side-channel
|
||||
|
|
@ -1051,6 +1255,100 @@ fn java_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) SQL recording: Java leg of the side-channel
|
||||
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
|
||||
// Rust SQL tests — the Java fragment never opens a live JDBC handle
|
||||
// (sqlite-jdbc is not stdlib; pulling it would force a classpath
|
||||
// prereq onto the dynamic CI matrix) so it surfaces the attempted
|
||||
// tautology query through the shim recorder as `driver = "manual"`.
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: java not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("java/sql/vuln/main.java.fragment"))
|
||||
.expect("read java sql 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_SQL_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(),
|
||||
"SqlStub must capture at least one event after the Java shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"detail map entries passed to __nyx_stub_sql_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: java not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("java/sql/vuln/main.java.fragment"))
|
||||
.expect("read java sql 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_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("java driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_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 java_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !java_available() {
|
||||
|
|
@ -1166,7 +1464,7 @@ fn rust_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
|||
|
||||
let crate_dir = workdir.path().join("driver");
|
||||
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("http"))
|
||||
.expect("write Cargo.toml");
|
||||
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||
|
||||
|
|
@ -1229,7 +1527,7 @@ fn rust_http_shim_recorder_is_noop_without_log_env() {
|
|||
|
||||
let crate_dir = workdir.path().join("driver_no_log");
|
||||
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("http_no_log"))
|
||||
.expect("write Cargo.toml");
|
||||
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||
|
||||
|
|
@ -1257,3 +1555,116 @@ fn rust_http_shim_recorder_is_noop_without_log_env() {
|
|||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) SQL recording: Rust leg of the side-channel
|
||||
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP
|
||||
// SQL tests — the Rust fragment never opens a live SQLite handle
|
||||
// (no stdlib driver; rusqlite would force libsqlite3-dev onto the
|
||||
// CI matrix) so it surfaces the attempted tautology query through
|
||||
// the shim recorder as `driver = "manual"`. Uses the same
|
||||
// `extra_files`-driven `Cargo.toml` shape as the HTTP siblings so
|
||||
// `cargo run --quiet` resolves `libc` (referenced by the spliced
|
||||
// probe shim's `__nyx_install_crash_guard`).
|
||||
if !cargo_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("rust/sql/vuln/main.rs"))
|
||||
.expect("read rust sql fragment");
|
||||
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
|
||||
|
||||
let crate_dir = workdir.path().join("driver_sql");
|
||||
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("sql"))
|
||||
.expect("write Cargo.toml");
|
||||
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.arg("run")
|
||||
.arg("--quiet")
|
||||
.arg("--manifest-path")
|
||||
.arg(crate_dir.join("Cargo.toml"))
|
||||
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env(recording.0, &recording.1)
|
||||
.output()
|
||||
.expect("cargo run rust sql driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"SqlStub must capture at least one event after the Rust shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"detail slice passed to __nyx_stub_sql_record must surface as event detail entries"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !cargo_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("rust/sql/vuln/main.rs"))
|
||||
.expect("read rust sql fragment");
|
||||
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
|
||||
|
||||
let crate_dir = workdir.path().join("driver_sql_no_log");
|
||||
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("sql_no_log"))
|
||||
.expect("write Cargo.toml");
|
||||
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.arg("run")
|
||||
.arg("--quiet")
|
||||
.arg("--manifest-path")
|
||||
.arg(crate_dir.join("Cargo.toml"))
|
||||
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
|
||||
.env("NYX_SQL_ENDPOINT", &endpoint)
|
||||
.env_remove("NYX_SQL_LOG")
|
||||
.output()
|
||||
.expect("cargo run rust sql driver");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_LOG; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue