//! Integration test for Phase 06 — Track C.1. //! //! Synthetic harness emits a structured [`SinkProbe`] record to the //! per-run [`ProbeChannel`]; the oracle's [`Oracle::SinkProbe`] path //! drains the channel and applies [`ProbePredicate`]s. A matching //! synthetic control harness *omits* the probe write — the same oracle //! must then return `NotConfirmed`. //! //! Acceptance bullet from `plan.md` phase 06: //! //! > Removing the probe write from one fixture flips its verdict from //! > `Confirmed` to `NotConfirmed` in CI. //! //! Mechanism: the two fixtures share the identical oracle + payload //! configuration; the only difference is whether the synthetic harness //! body writes a [`SinkProbe`] record to the probe channel. #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ PROBE_PATH_ENV, ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe, }; use std::sync::{Mutex, MutexGuard}; use std::time::Duration; use tempfile::TempDir; static PROBE_ENV_LOCK: Mutex<()> = Mutex::new(()); struct ProbeEnvGuard { _lock: MutexGuard<'static, ()>, prior: Option, } impl ProbeEnvGuard { fn set(channel: &ProbeChannel) -> Self { let lock = PROBE_ENV_LOCK .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); let prior = std::env::var(PROBE_PATH_ENV).ok(); unsafe { std::env::set_var(PROBE_PATH_ENV, channel.path()) }; Self { _lock: lock, prior } } } impl Drop for ProbeEnvGuard { fn drop(&mut self) { match self.prior.take() { Some(value) => unsafe { std::env::set_var(PROBE_PATH_ENV, value) }, None => unsafe { std::env::remove_var(PROBE_PATH_ENV) }, } } } /// Minimal [`SandboxOutcome`] suitable for oracle evaluation when the /// runner-side execution path is not exercised. All flags are off so any /// `true` verdict must come from the probe channel, not from /// `output_contains` / `oob_callback_seen` etc. fn dummy_outcome() -> nyx_scanner::dynamic::sandbox::SandboxOutcome { nyx_scanner::dynamic::sandbox::SandboxOutcome { exit_code: Some(0), stdout: vec![], stderr: vec![], timed_out: false, oob_callback_seen: false, sink_hit: true, duration: Duration::from_millis(1), hardening_outcome: None, } } /// Synthetic harness body. Mirrors what a real per-language `__nyx_probe` /// shim would do: read `NYX_PROBE_PATH` from its env, append one JSON /// record per fired sink. The runner-side test serialises the harness /// invocation with this Rust function instead of spawning a subprocess. fn synthetic_harness_fires_probe( channel: &ProbeChannel, sink_callee: &str, captured_arg: &str, payload_id: &str, ) { let probe = SinkProbe { sink_callee: sink_callee.into(), args: vec![ProbeArg::String(captured_arg.into())], captured_at_ns: 1, payload_id: payload_id.into(), kind: ProbeKind::Normal, witness: ProbeWitness::empty(), }; channel .write(&probe) .expect("synthetic harness probe write"); } /// "Control" harness — runs the same way but does NOT write a probe. fn synthetic_harness_omits_probe(_channel: &ProbeChannel) { // Intentionally empty: the oracle path must observe zero probe records // and decide NotConfirmed. } #[test] fn sink_probe_oracle_confirms_when_harness_writes_probe() { let dir = TempDir::new().unwrap(); let channel = ProbeChannel::for_workdir(dir.path()).unwrap(); // Exercise the harness env-var path so the test also locks the // NYX_PROBE_PATH contract the real sandbox forwards to the harness. let _env = ProbeEnvGuard::set(&channel); assert_eq!( std::env::var(PROBE_PATH_ENV).unwrap().as_str(), channel.path().to_str().unwrap(), ); synthetic_harness_fires_probe( &channel, "os.system", "; echo NYX_PWN_CMDI", "cmdi-echo-marker", ); let oracle = Oracle::SinkProbe { predicates: &[ ProbePredicate::CalleeEquals("os.system"), ProbePredicate::ArgContains { index: 0, needle: "NYX_PWN_CMDI", }, ], }; let probes = channel.drain(); assert_eq!(probes.len(), 1, "harness must have written one probe"); assert!( oracle_fired(&oracle, &dummy_outcome(), &probes), "oracle with SinkProbe predicates must confirm when probe matches", ); } #[test] fn sink_probe_oracle_not_confirmed_when_harness_omits_probe() { let dir = TempDir::new().unwrap(); let channel = ProbeChannel::for_workdir(dir.path()).unwrap(); let _env = ProbeEnvGuard::set(&channel); // Control fixture: identical configuration but the harness skips its // probe write. Same oracle predicate set as the Confirmed test — // the only difference is the (absent) write. synthetic_harness_omits_probe(&channel); let oracle = Oracle::SinkProbe { predicates: &[ ProbePredicate::CalleeEquals("os.system"), ProbePredicate::ArgContains { index: 0, needle: "NYX_PWN_CMDI", }, ], }; let probes = channel.drain(); assert!( probes.is_empty(), "control harness must not have written any probe", ); assert!( !oracle_fired(&oracle, &dummy_outcome(), &probes), "oracle must NOT confirm when no probe is present", ); } #[test] fn sink_probe_oracle_not_confirmed_when_predicate_mismatch() { // Probe is present, but its captured arg does not satisfy the // predicates. Verifies the oracle does not blanket-confirm on // "any probe at all" — payload predicates have teeth. let dir = TempDir::new().unwrap(); let channel = ProbeChannel::for_workdir(dir.path()).unwrap(); synthetic_harness_fires_probe( &channel, "os.system", "benign argument that does not match", "cmdi-echo-marker", ); let oracle = Oracle::SinkProbe { predicates: &[ProbePredicate::ArgContains { index: 0, needle: "NYX_PWN_CMDI", }], }; let probes = channel.drain(); assert_eq!(probes.len(), 1); assert!( !oracle_fired(&oracle, &dummy_outcome(), &probes), "oracle must NOT confirm when probe args fail the predicate set", ); } #[test] fn probe_channel_clear_between_runs_isolates_verdicts() { // Mirrors the runner's clear-before-each-payload behaviour: a probe // left over from a previous payload run must not bleed into the // verdict for a later payload. let dir = TempDir::new().unwrap(); let channel = ProbeChannel::for_workdir(dir.path()).unwrap(); synthetic_harness_fires_probe(&channel, "os.system", "stale probe", "earlier-payload"); assert_eq!(channel.drain().len(), 1); channel.clear().unwrap(); assert!( channel.drain().is_empty(), "clear() must remove the leftover probe from the previous run", ); let oracle = Oracle::SinkProbe { predicates: &[ProbePredicate::CalleeEquals("os.system")], }; // Second payload omits the probe write entirely. let probes = channel.drain(); assert!(!oracle_fired(&oracle, &dummy_outcome(), &probes)); }