From 6d0e4a5afd85fd0d8c920cd45a5134b21a93c074 Mon Sep 17 00:00:00 2001 From: elipeter Date: Mon, 25 May 2026 09:17:57 -0500 Subject: [PATCH] refactor(dynamic): add Phase 17 end-to-end tests for Go and Rust frameworks, improve fixture coverage, and enhance test modularity --- tests/go_frameworks_corpus.rs | 171 +++++++++++++++++++++++++++ tests/rust_frameworks_corpus.rs | 198 ++++++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) diff --git a/tests/go_frameworks_corpus.rs b/tests/go_frameworks_corpus.rs index 3895f436..9ad98d1b 100644 --- a/tests/go_frameworks_corpus.rs +++ b/tests/go_frameworks_corpus.rs @@ -11,6 +11,8 @@ #![cfg(feature = "dynamic")] +mod common; + use nyx_scanner::dynamic::framework::{HttpMethod, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; @@ -142,3 +144,172 @@ fn gin_adapter_rejects_cache_get_receiver_collision() { "cache.Get must not be treated as a gin route registration" ); } + +// ── End-to-end Phase 17 dispatcher acceptance via run_spec ───────────────── + +#[cfg(test)] +mod e2e_phase_17 { + use super::*; + use crate::common::fixture_harness::FIXTURE_LOCK; + use nyx_scanner::dynamic::framework::{FrameworkBinding, RouteShape}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; + use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; + use nyx_scanner::dynamic::spec::{ + HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, + }; + use nyx_scanner::evidence::DifferentialVerdict; + use nyx_scanner::labels::Cap; + use std::path::PathBuf; + use std::process::Command; + use tempfile::TempDir; + + #[derive(Clone, Copy)] + struct Case { + fixture_dir: &'static str, + adapter: &'static str, + } + + const CASES: &[Case] = &[ + Case { + fixture_dir: "gin", + adapter: "go-gin", + }, + Case { + fixture_dir: "echo", + adapter: "go-echo", + }, + Case { + fixture_dir: "fiber", + adapter: "go-fiber", + }, + Case { + fixture_dir: "chi", + adapter: "go-chi", + }, + ]; + + fn command_available(bin: &str) -> bool { + Command::new(bin) + .arg("version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + fn build_spec(case: Case, fixture_file: &str) -> (HarnessSpec, TempDir) { + let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/dynamic_fixtures/go_frameworks") + .join(case.fixture_dir) + .join(fixture_file); + let tmp = TempDir::new().expect("create tempdir"); + let dst = tmp.path().join(fixture_file); + std::fs::copy(&src, &dst).expect("copy fixture into tempdir"); + + let entry_file = dst.to_string_lossy().into_owned(); + let mut digest = blake3::Hasher::new(); + digest.update(b"phase17-go-framework|"); + digest.update(case.fixture_dir.as_bytes()); + digest.update(b"|"); + digest.update(fixture_file.as_bytes()); + let spec_hash = format!("{:016x}", { + let bytes = digest.finalize(); + u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap()) + }); + + let framework = Some(FrameworkBinding { + adapter: case.adapter.to_owned(), + kind: EntryKind::HttpRoute, + route: Some(RouteShape::single(HttpMethod::GET, "/run")), + request_params: vec![], + response_writer: None, + middleware: vec![], + }); + + let spec = HarnessSpec { + finding_id: spec_hash.clone(), + entry_file: entry_file.clone(), + entry_name: "Run".to_owned(), + entry_kind: EntryKind::HttpRoute, + lang: Lang::Go, + toolchain_id: default_toolchain_id(Lang::Go).to_owned(), + payload_slot: PayloadSlot::QueryParam("cmd".to_owned()), + expected_cap: Cap::CODE_EXEC, + constraint_hints: vec![], + sink_file: entry_file, + sink_line: 1, + spec_hash, + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework, + java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), + }; + (spec, tmp) + } + + fn run(case: Case, fixture_file: &str) -> Option { + if !command_available("go") { + eprintln!( + "SKIP Go {}/{fixture_file}: missing toolchain go", + case.fixture_dir + ); + return None; + } + + let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let (spec, _tmp) = build_spec(case, fixture_file); + let opts = SandboxOptions { + backend: SandboxBackend::Process, + ..SandboxOptions::default() + }; + match run_spec(&spec, &opts) { + Ok(outcome) => Some(outcome), + Err(RunError::BuildFailed { stderr, attempts }) => { + eprintln!( + "SKIP Go {}/{fixture_file}: harness build failed after {attempts} attempts: {stderr}", + case.fixture_dir, + ); + None + } + Err(e) => panic!( + "run_spec(Go {}/{fixture_file}) errored: {e:?}", + case.fixture_dir + ), + } + } + + #[test] + fn go_framework_vuln_fixtures_confirm_via_run_spec() { + for case in CASES { + let Some(outcome) = run(*case, "vuln.go") else { + continue; + }; + assert!( + outcome.triggered_by.is_some(), + "{} vuln must Confirm via run_spec; got {outcome:?}", + case.adapter, + ); + let diff = outcome + .differential + .as_ref() + .expect("Confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } + } + + #[test] + fn go_framework_benign_fixtures_do_not_confirm_via_run_spec() { + for case in CASES { + let Some(outcome) = run(*case, "benign.go") else { + continue; + }; + assert!( + outcome.triggered_by.is_none(), + "{} benign control must not Confirm via run_spec; got {outcome:?}", + case.adapter, + ); + if let Some(diff) = outcome.differential.as_ref() { + assert_ne!(diff.verdict, DifferentialVerdict::Confirmed); + } + } + } +} diff --git a/tests/rust_frameworks_corpus.rs b/tests/rust_frameworks_corpus.rs index a62900fb..55a6b5a7 100644 --- a/tests/rust_frameworks_corpus.rs +++ b/tests/rust_frameworks_corpus.rs @@ -11,6 +11,8 @@ #![cfg(feature = "dynamic")] +mod common; + use nyx_scanner::dynamic::framework::{HttpMethod, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; @@ -138,3 +140,199 @@ fn axum_adapter_ignores_unrelated_function() { let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Rust); assert!(binding.is_none()); } + +// ── End-to-end Phase 17 dispatcher acceptance via run_spec ───────────────── + +#[cfg(test)] +mod e2e_phase_17 { + use super::*; + use crate::common::fixture_harness::FIXTURE_LOCK; + use nyx_scanner::dynamic::framework::{FrameworkBinding, RouteShape}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; + use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; + use nyx_scanner::dynamic::spec::{ + HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, + }; + use nyx_scanner::evidence::DifferentialVerdict; + use nyx_scanner::labels::Cap; + use std::path::PathBuf; + use std::process::Command; + use tempfile::TempDir; + + #[derive(Clone, Copy)] + struct Case { + fixture_dir: &'static str, + adapter: &'static str, + expected_path_fragment: &'static str, + } + + const CASES: &[Case] = &[ + Case { + fixture_dir: "axum", + adapter: "rust-axum", + expected_path_fragment: "/run", + }, + Case { + fixture_dir: "actix", + adapter: "rust-actix", + expected_path_fragment: "/run", + }, + Case { + fixture_dir: "rocket", + adapter: "rust-rocket", + expected_path_fragment: "/run", + }, + Case { + fixture_dir: "warp", + adapter: "rust-warp", + expected_path_fragment: "run", + }, + ]; + + fn command_available(bin: &str) -> bool { + Command::new(bin) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + fn build_spec(case: Case, fixture_file: &str) -> (HarnessSpec, TempDir) { + let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/dynamic_fixtures/rust_frameworks") + .join(case.fixture_dir) + .join(fixture_file); + let tmp = TempDir::new().expect("create tempdir"); + let dst = tmp.path().join(fixture_file); + std::fs::copy(&src, &dst).expect("copy fixture into tempdir"); + + let entry_file = dst.to_string_lossy().into_owned(); + let mut digest = blake3::Hasher::new(); + digest.update(b"phase17-rust-framework|"); + digest.update(case.fixture_dir.as_bytes()); + digest.update(b"|"); + digest.update(fixture_file.as_bytes()); + let spec_hash = format!("{:016x}", { + let bytes = digest.finalize(); + u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap()) + }); + + let framework = Some(FrameworkBinding { + adapter: case.adapter.to_owned(), + kind: EntryKind::HttpRoute, + route: Some(RouteShape::single( + HttpMethod::GET, + case.expected_path_fragment, + )), + request_params: vec![], + response_writer: None, + middleware: vec![], + }); + + let spec = HarnessSpec { + finding_id: spec_hash.clone(), + entry_file: entry_file.clone(), + entry_name: "run".to_owned(), + entry_kind: EntryKind::HttpRoute, + lang: Lang::Rust, + toolchain_id: default_toolchain_id(Lang::Rust).to_owned(), + payload_slot: PayloadSlot::QueryParam("cmd".to_owned()), + expected_cap: Cap::CODE_EXEC, + constraint_hints: vec![], + sink_file: entry_file, + sink_line: 1, + spec_hash, + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework, + java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), + }; + (spec, tmp) + } + + fn run(case: Case, fixture_file: &str) -> Option { + if !command_available("cargo") { + eprintln!( + "SKIP Rust {}/{fixture_file}: missing toolchain cargo", + case.fixture_dir + ); + return None; + } + + let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let (spec, tmp) = build_spec(case, fixture_file); + let repro = tmp.path().join("repro"); + let telemetry = tmp.path().join("events.jsonl"); + unsafe { + std::env::set_var("NYX_REPRO_BASE", repro.to_str().unwrap()); + std::env::set_var("NYX_TELEMETRY_PATH", telemetry.to_str().unwrap()); + } + let opts = SandboxOptions { + backend: SandboxBackend::Process, + ..SandboxOptions::default() + }; + let outcome = run_spec(&spec, &opts); + unsafe { + std::env::remove_var("NYX_REPRO_BASE"); + std::env::remove_var("NYX_TELEMETRY_PATH"); + } + + match outcome { + Ok(outcome) => Some(outcome), + Err(RunError::BuildFailed { stderr, attempts }) => { + eprintln!( + "SKIP Rust {}/{fixture_file}: harness build failed after {attempts} attempts: {stderr}", + case.fixture_dir, + ); + None + } + Err(RunError::Sandbox(e)) => { + eprintln!( + "SKIP Rust {}/{fixture_file}: harness sandbox failed before verdict: {e:?}", + case.fixture_dir, + ); + None + } + Err(e) => panic!( + "run_spec(Rust {}/{fixture_file}) errored: {e:?}", + case.fixture_dir + ), + } + } + + #[test] + fn rust_framework_vuln_fixtures_confirm_via_run_spec() { + for case in CASES { + let Some(outcome) = run(*case, "vuln.rs") else { + continue; + }; + assert!( + outcome.triggered_by.is_some(), + "{} vuln must Confirm via run_spec; got {outcome:?}", + case.adapter, + ); + let diff = outcome + .differential + .as_ref() + .expect("Confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } + } + + #[test] + fn rust_framework_benign_fixtures_do_not_confirm_via_run_spec() { + for case in CASES { + let Some(outcome) = run(*case, "benign.rs") else { + continue; + }; + assert!( + outcome.triggered_by.is_none(), + "{} benign control must not Confirm via run_spec; got {outcome:?}", + case.adapter, + ); + if let Some(diff) = outcome.differential.as_ref() { + assert_ne!(diff.verdict, DifferentialVerdict::Confirmed); + } + } + } +}