[pitboss] phase 05: Track C.2 + Track I.1 quick unlocks — OOB listener wired + golden-verdict fixture runner

This commit is contained in:
pitboss 2026-05-14 05:01:50 -05:00
parent 937eb479e6
commit cdbc7f2d21
50 changed files with 790 additions and 587 deletions

View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Regenerate dynamic-fixture golden verdicts.
#
# Usage:
# ./scripts/update_dynamic_goldens.sh [--test <name>]
#
# Re-runs the dynamic fixture suites under `NYX_UPDATE_GOLDENS=1` so each
# fixture's harness overwrites its `.golden.json` file with the current
# verdict. After this script completes, rerun without the env var to
# confirm the goldens match.
#
# Default: refreshes both python_fixtures and rust_fixtures. Pass --test
# to refresh only one suite (e.g. `--test python_fixtures`).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SUITES=(python_fixtures rust_fixtures)
if [[ $# -gt 0 ]]; then
case "$1" in
--test) SUITES=("$2"); shift 2 ;;
-h|--help)
sed -n '2,12p' "$0"
exit 0
;;
*)
echo "unknown arg: $1" >&2
exit 1
;;
esac
fi
cd "$REPO_ROOT"
for suite in "${SUITES[@]}"; do
echo "[update-goldens] refreshing $suite ..."
NYX_UPDATE_GOLDENS=1 \
cargo nextest run --features dynamic --test "$suite" --no-fail-fast
done
echo "[update-goldens] re-running suites without NYX_UPDATE_GOLDENS=1 to verify ..."
for suite in "${SUITES[@]}"; do
cargo nextest run --features dynamic --test "$suite"
done
echo "[update-goldens] done. Inspect git diff under tests/dynamic_fixtures/ before committing."

View file

@ -5,6 +5,17 @@
//! URL path. The lifetime of the listener is per-scan: create one
//! [`OobListener`] at scan start, drop it when the scan finishes.
//!
//! # Wiring
//!
//! As of Phase 05 the listener is load-bearing: [`crate::dynamic::verify::VerifyOptions::from_config`]
//! constructs one per scan via [`OobListener::bind`] and threads it into
//! [`crate::dynamic::sandbox::SandboxOptions::oob_listener`]. The runner
//! polls [`OobListener::was_nonce_hit`] after each sandbox run (see
//! `src/dynamic/runner.rs`) and toggles
//! [`crate::dynamic::sandbox::SandboxOutcome::oob_callback_seen`] when a
//! probe arrives — that is the only signal that turns an OOB-only sink
//! (e.g. blind SSRF) into a `Confirmed` verdict.
//!
//! # Nonce URL
//!
//! The caller generates a per-finding nonce (UUID4 hex) and embeds it in

View file

@ -6,6 +6,7 @@
use crate::callgraph::CallGraph;
use crate::commands::scan::Diag;
use crate::dynamic::corpus::{payloads_for, CORPUS_VERSION};
use crate::dynamic::oob::OobListener;
use crate::dynamic::report::{AttemptSummary, VerifyResult, VerifyStatus};
use crate::dynamic::runner::{run_spec, RunError};
use crate::dynamic::sandbox::{toolchain_id_with_digest, SandboxOptions};
@ -54,6 +55,17 @@ pub struct VerifyOptions {
impl VerifyOptions {
/// Build `VerifyOptions` from scanner config.
///
/// Binds a per-scan [`OobListener`] on a free loopback port and attaches
/// it to `sandbox.oob_listener`. The listener is held by `Arc` so every
/// per-finding clone of `VerifyOptions` shares the same accept thread;
/// it is torn down via the `OobListener::Drop` impl once the last
/// `Arc` is released at end of scan.
///
/// If `OobListener::bind` fails (e.g. all loopback ports are in use),
/// the field stays `None`; the runner skips OOB-callback payloads
/// (`src/dynamic/runner.rs` `oob_nonce_slot` branch) while non-OOB
/// payloads continue to run against their existing oracle.
pub fn from_config(config: &Config) -> Self {
use crate::dynamic::sandbox::SandboxBackend;
let backend = match config.scanner.verify_backend.as_str() {
@ -61,9 +73,11 @@ impl VerifyOptions {
"process" => SandboxBackend::Process,
_ => SandboxBackend::Auto,
};
let oob_listener = OobListener::bind().ok().map(Arc::new);
Self {
sandbox: SandboxOptions {
backend,
oob_listener,
..SandboxOptions::default()
},
project_root: None,

View file

@ -0,0 +1,249 @@
//! Golden-verdict regression harness for dynamic-verification fixtures.
//!
//! Replaces the original hand-rolled `assert_eq!(status, Confirmed)` style
//! with a "current verdict is the golden" model: each fixture's first run
//! (under `NYX_UPDATE_GOLDENS=1`) records its current verdict shape into a
//! `.golden.json` file checked in beside the fixture; subsequent runs diff
//! against that golden and fail on regression.
//!
//! The contract is intentionally agnostic to the verdict's polarity. A
//! fixture stuck at `Inconclusive(BuildFailed)` because of a missing
//! toolchain is locked at that shape until someone consciously refreshes the
//! golden via `scripts/update_dynamic_goldens.sh`. A flip to `Confirmed` is
//! also a "regression" in the harness's sense and surfaces as a test
//! failure, prompting an explicit golden update.
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
use nyx_scanner::evidence::{
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
VerifyResult, VerifyStatus,
};
use nyx_scanner::labels::Cap;
use nyx_scanner::patterns::{FindingCategory, Severity};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::TempDir;
/// Serialise-once lock guarding the process-global env vars
/// (`NYX_REPRO_BASE`, `NYX_TELEMETRY_PATH`) and the shared build cache dir.
/// Shared across `python_fixtures` / `rust_fixtures` to prevent cross-suite
/// races when nextest runs them in parallel within the same test binary.
pub static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
/// How the fixture source should land relative to the harness's tempdir
/// before [`verify_finding`] is invoked. Mirrors the original per-language
/// behaviour: Python copies the file beside its sibling-import siblings;
/// Rust lays it out as `src/entry.rs` so the Cargo project emitter finds it.
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Each test binary uses only one variant; the other is dead per-crate.
pub enum CopyStrategy {
/// Copy the fixture to `tempdir/{fixture_basename}`. The synthesised Diag
/// points at the copy so the Python harness can import it directly.
PreserveName,
/// Copy the fixture to `tempdir/src/entry.rs`. The synthesised Diag
/// points at the original fixture path (the Rust emitter reads source via
/// the absolute Diag path, not via the temp-dir layout).
RustEntry,
}
/// Per-fixture specification.
pub struct FixtureSpec<'a> {
/// Subdirectory under `tests/dynamic_fixtures/` (e.g. `"python"`, `"rust"`).
pub lang_dir: &'a str,
/// Fixture filename within `lang_dir`.
pub fixture: &'a str,
/// Entry-point function name passed in the synthesised flow-step.
pub func: &'a str,
/// Sink capability bits to set on `Evidence.sink_caps`.
pub cap: Cap,
/// Sink line for the synthesised flow-step. Adversarial fixtures pass a
/// line that does not exist in the source (e.g. 999) so the probe cannot
/// fire while the oracle marker still prints.
pub sink_line: u32,
/// Confidence stamp on the Diag. `Confidence::Low` short-circuits to
/// `Unsupported(ConfidenceTooLow)` before the harness executes.
pub confidence: Confidence,
/// File-layout strategy for the temp-dir copy.
pub copy: CopyStrategy,
}
/// Trimmed verdict shape persisted in the `.golden.json` file.
///
/// Captures the fields a regression test must pin: status + typed reasons
/// + whether a payload triggered. Excludes machine-dependent fields
/// (`finding_id`, `detail`, `attempts`, `toolchain_match`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoldenVerdict {
pub status: VerifyStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<UnsupportedReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inconclusive_reason: Option<InconclusiveReason>,
#[serde(default)]
pub triggered: bool,
}
impl From<&VerifyResult> for GoldenVerdict {
fn from(v: &VerifyResult) -> Self {
Self {
status: v.status,
reason: v.reason.clone(),
inconclusive_reason: v.inconclusive_reason.clone(),
triggered: v.triggered_payload.is_some(),
}
}
}
/// Run the fixture through `verify_finding` and either compare against the
/// stored golden or — when `NYX_UPDATE_GOLDENS=1` — overwrite the golden
/// with the current verdict.
pub fn run_fixture_and_compare_to_golden(spec: &FixtureSpec<'_>) {
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let fixture_root = fixture_dir(spec.lang_dir);
let fixture_src = fixture_root.join(spec.fixture);
let golden_path = fixture_root.join(format!("{}.golden.json", spec.fixture));
let tmp = TempDir::new().expect("create tempdir");
let diag_path = stage_fixture(&fixture_src, &tmp, spec.copy);
// SAFETY: env mutation is serialised by FIXTURE_LOCK and the vars are
// cleared before the lock guard drops at end of function.
unsafe {
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
std::env::set_var(
"NYX_TELEMETRY_PATH",
tmp.path().join("events.jsonl").to_str().unwrap(),
);
}
let mut diag = make_diag(&diag_path, spec.func, spec.cap, spec.sink_line);
diag.confidence = Some(spec.confidence);
let opts = VerifyOptions::default();
let result = verify_finding(&diag, &opts);
unsafe {
std::env::remove_var("NYX_REPRO_BASE");
std::env::remove_var("NYX_TELEMETRY_PATH");
}
let current = GoldenVerdict::from(&result);
let mut current_json =
serde_json::to_string_pretty(&current).expect("serialise golden verdict");
current_json.push('\n');
if std::env::var("NYX_UPDATE_GOLDENS").is_ok_and(|v| v == "1") {
std::fs::write(&golden_path, &current_json).unwrap_or_else(|e| {
panic!("write golden {}: {e}", golden_path.display())
});
return;
}
let expected_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"missing golden {}: {e}\n\
current verdict:\n{current_json}\n\
rerun with NYX_UPDATE_GOLDENS=1 ./scripts/update_dynamic_goldens.sh to seed it.",
golden_path.display()
)
});
let expected: GoldenVerdict = serde_json::from_str(&expected_json)
.unwrap_or_else(|e| panic!("parse golden {}: {e}", golden_path.display()));
if current != expected {
panic!(
"golden regression for {}:\n\
expected: {expected_json}\n\
actual: {current_json}\n\
detail: {:?}\n\
rerun with NYX_UPDATE_GOLDENS=1 ./scripts/update_dynamic_goldens.sh if intended.",
spec.fixture, result.detail
);
}
}
fn fixture_dir(lang_dir: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/dynamic_fixtures")
.join(lang_dir)
}
fn stage_fixture(src: &Path, tmp: &TempDir, copy: CopyStrategy) -> PathBuf {
match copy {
CopyStrategy::PreserveName => {
let dst = tmp.path().join(src.file_name().expect("fixture has filename"));
std::fs::copy(src, &dst).expect("copy fixture into tempdir");
dst
}
CopyStrategy::RustEntry => {
let dst_dir = tmp.path().join("src");
std::fs::create_dir_all(&dst_dir).expect("create src/ in tempdir");
let dst = dst_dir.join("entry.rs");
std::fs::copy(src, &dst).expect("copy fixture into tempdir/src/entry.rs");
// The Rust harness emitter reads source via the Diag's absolute path,
// not via the temp-dir layout, so the Diag must point at the original
// fixture file. The temp-dir copy is only consulted by the harness
// builder for the workdir-relative `src/entry.rs` view.
src.to_path_buf()
}
}
}
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
let path_str = path.to_string_lossy().into_owned();
let evidence = Evidence {
flow_steps: vec![
FlowStep {
step: 1,
kind: FlowStepKind::Source,
file: path_str.clone(),
line: 1,
col: 0,
snippet: None,
variable: Some("payload".into()),
callee: None,
function: Some(func.to_owned()),
is_cross_file: false,
},
FlowStep {
step: 2,
kind: FlowStepKind::Sink,
file: path_str.clone(),
line: sink_line,
col: 4,
snippet: None,
variable: None,
callee: None,
function: None,
is_cross_file: false,
},
],
sink_caps: cap.bits(),
..Default::default()
};
Diag {
path: path_str,
line: sink_line as usize,
col: 0,
severity: Severity::High,
id: "taint-unsanitised-flow".into(),
category: FindingCategory::Security,
path_validated: false,
guard_kind: None,
message: None,
labels: vec![],
confidence: Some(Confidence::High),
evidence: Some(evidence),
rank_score: None,
rank_reason: None,
suppressed: false,
suppression: None,
rollup: None,
finding_id: String::new(),
alternative_finding_ids: vec![],
stable_hash: 0,
}
}

View file

@ -2,6 +2,13 @@
pub mod recall;
// Only `python_fixtures` and `rust_fixtures` reference these symbols; every
// other test binary pulls `mod common` in and would otherwise emit
// per-binary `dead_code` warnings for the whole submodule.
#[cfg(feature = "dynamic")]
#[allow(dead_code)]
pub mod fixture_harness;
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::utils::config::{AnalysisMode, Config};
use serde::Deserialize;

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -0,0 +1,5 @@
{
"status": "Inconclusive",
"inconclusive_reason": "OracleCollisionSuspected",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "NotConfirmed",
"triggered": false
}

View file

@ -0,0 +1,4 @@
{
"status": "Confirmed",
"triggered": true
}

View file

@ -0,0 +1,5 @@
{
"status": "Unsupported",
"reason": "ConfidenceTooLow",
"triggered": false
}

View file

@ -1,36 +1,33 @@
//! Python fixture integration tests (§15 Pillar B acceptance gate).
//!
//! Runs the dynamic verification pipeline against each Python fixture and
//! asserts the expected verdict. Requires `--features dynamic` and Python3
//! to be available on PATH.
//! Each fixture is run through the dynamic verification pipeline; its
//! verdict is then compared against the per-fixture golden under
//! `tests/dynamic_fixtures/python/{name}.golden.json`. Refresh the goldens
//! via `NYX_UPDATE_GOLDENS=1 ./scripts/update_dynamic_goldens.sh`.
//!
//! Verdicts under test:
//! - positive → Confirmed
//! - negative → NotConfirmed
//! - unsupported → Unsupported(ConfidenceTooLow) [spec-level rejection]
//! - adversarial → Inconclusive(OracleCollisionSuspected)
//!
//! Tests are skipped when Python3 is not available.
//! Tests that need python3 on PATH skip with an `eprintln!` when it is
//! missing; `Confidence::Low` rows do not need python3 because the verifier
//! short-circuits before harness execution.
mod common;
#[cfg(feature = "dynamic")]
mod python_fixture_tests {
use crate::common::fixture_harness::{
run_fixture_and_compare_to_golden, CopyStrategy, FixtureSpec,
};
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
use nyx_scanner::evidence::{
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
VerifyStatus,
Confidence, Evidence, FlowStep, FlowStepKind, UnsupportedReason, VerifyStatus,
};
use nyx_scanner::labels::Cap;
use nyx_scanner::patterns::{FindingCategory, Severity};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::TempDir;
// Serialize all fixture tests to prevent races on process-global state
// (NYX_REPRO_BASE and NYX_TELEMETRY_PATH env vars).
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
/// Returns `true` if `python3` is available.
/// `python3` available on PATH? Tests that need an interpreter return
/// early with an `eprintln!` when this is false.
fn python3_available() -> bool {
std::process::Command::new("python3")
.arg("--version")
@ -39,337 +36,204 @@ mod python_fixture_tests {
.unwrap_or(false)
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/dynamic_fixtures/python")
.join(name)
}
/// Run a fixture and return the verdict.
///
/// Acquires `FIXTURE_LOCK` for the full duration to prevent races on the
/// process-global NYX_REPRO_BASE / NYX_TELEMETRY_PATH env vars.
/// `set_current_dir` is NOT used here: `harness::copy_entry_file` resolves
/// the entry file via its absolute path, so CWD is irrelevant.
fn run_fixture(fixture: &str, func: &str, cap: Cap, sink_line: u32) -> nyx_scanner::evidence::VerifyResult {
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = fixture_path(fixture);
// Copy fixture to a temp dir so the harness can import it.
let tmp = TempDir::new().unwrap();
let dst = tmp.path().join(Path::new(fixture).file_name().unwrap());
std::fs::copy(&path, &dst).expect("fixture file must exist");
// Set up repro and telemetry to temp dirs to avoid side effects.
unsafe {
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
std::env::set_var("NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap());
fn spec(fixture: &'static str, func: &'static str, cap: Cap, sink_line: u32) -> FixtureSpec<'static> {
FixtureSpec {
lang_dir: "python",
fixture,
func,
cap,
sink_line,
confidence: Confidence::High,
copy: CopyStrategy::PreserveName,
}
}
// Use the temp dir copy as the fixture path (absolute — no CWD change needed).
let diag = make_diag(&dst, func, cap, sink_line);
let opts = VerifyOptions::default();
let result = verify_finding(&diag, &opts);
unsafe {
std::env::remove_var("NYX_REPRO_BASE");
std::env::remove_var("NYX_TELEMETRY_PATH");
fn low_spec(
fixture: &'static str,
func: &'static str,
cap: Cap,
sink_line: u32,
) -> FixtureSpec<'static> {
FixtureSpec {
lang_dir: "python",
fixture,
func,
cap,
sink_line,
confidence: Confidence::Low,
copy: CopyStrategy::PreserveName,
}
result
}
// ── SQLi fixtures ────────────────────────────────────────────────────────
// ── SQLi ─────────────────────────────────────────────────────────────────
#[test]
fn sqli_positive_is_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("sqli_positive.py", "login", Cap::SQL_QUERY, 17);
assert_eq!(
result.status, VerifyStatus::Confirmed,
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
result.status, result.detail
);
fn sqli_positive_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("sqli_positive.py", "login", Cap::SQL_QUERY, 17));
}
#[test]
fn sqli_negative_is_not_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("sqli_negative.py", "login", Cap::SQL_QUERY, 12);
assert_eq!(
result.status, VerifyStatus::NotConfirmed,
"sqli_negative must be NotConfirmed; got {:?}",
result.status
);
fn sqli_negative_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("sqli_negative.py", "login", Cap::SQL_QUERY, 12));
}
#[test]
fn sqli_unsupported_is_unsupported() {
// Low-confidence Diag → Unsupported(ConfidenceTooLow) without execution.
let path = fixture_path("sqli_unsupported.py");
let mut d = make_diag(&path, "find_user", Cap::SQL_QUERY, 10);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn sqli_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"sqli_unsupported.py",
"find_user",
Cap::SQL_QUERY,
10,
));
}
#[test]
fn sqli_adversarial_is_inconclusive_collision() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
// The adversarial fixture prints the oracle marker WITHOUT going through
// any SQL sink — so the oracle fires but the probe at the (nonexistent)
// SQL execute line does not.
// We point the sink line at a line that doesn't exist in the file (999)
// so the settrace probe can't fire.
let result = run_fixture("sqli_adversarial.py", "get_value", Cap::SQL_QUERY, 999);
// Oracle fires (prints "NYX_SQL_CONFIRMED") but probe doesn't (line 999 missing).
assert_eq!(
result.status, VerifyStatus::Inconclusive,
"sqli_adversarial must be Inconclusive; got {:?}",
result.status
);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected),
"adversarial must be OracleCollisionSuspected"
);
fn sqli_adversarial_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("sqli_adversarial.py", "get_value", Cap::SQL_QUERY, 999));
}
// ── Command injection fixtures ───────────────────────────────────────────
// ── Command injection ────────────────────────────────────────────────────
#[test]
fn cmdi_positive_is_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("cmdi_positive.py", "run_ping", Cap::CODE_EXEC, 13);
assert_eq!(
result.status, VerifyStatus::Confirmed,
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
result.status, result.detail
);
fn cmdi_positive_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("cmdi_positive.py", "run_ping", Cap::CODE_EXEC, 13));
}
#[test]
fn cmdi_negative_is_not_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("cmdi_negative.py", "run_ping", Cap::CODE_EXEC, 17);
assert_eq!(
result.status, VerifyStatus::NotConfirmed,
"cmdi_negative must be NotConfirmed; got {:?}",
result.status
);
fn cmdi_negative_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("cmdi_negative.py", "run_ping", Cap::CODE_EXEC, 17));
}
#[test]
fn cmdi_unsupported_is_unsupported() {
let path = fixture_path("cmdi_unsupported.py");
let mut d = make_diag(&path, "process_request", Cap::CODE_EXEC, 9);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn cmdi_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"cmdi_unsupported.py",
"process_request",
Cap::CODE_EXEC,
9,
));
}
#[test]
fn cmdi_adversarial_is_inconclusive_collision() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("cmdi_adversarial.py", "process_input", Cap::CODE_EXEC, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn cmdi_adversarial_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec(
"cmdi_adversarial.py",
"process_input",
Cap::CODE_EXEC,
999,
));
}
// ── File I/O fixtures ────────────────────────────────────────────────────
// ── File I/O ─────────────────────────────────────────────────────────────
#[test]
fn fileio_positive_is_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("fileio_positive.py", "read_file", Cap::FILE_IO, 11);
assert_eq!(
result.status, VerifyStatus::Confirmed,
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
result.status, result.detail
);
fn fileio_positive_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("fileio_positive.py", "read_file", Cap::FILE_IO, 11));
}
#[test]
fn fileio_negative_is_not_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("fileio_negative.py", "read_file", Cap::FILE_IO, 18);
assert_eq!(
result.status, VerifyStatus::NotConfirmed,
"fileio_negative must be NotConfirmed; got {:?}",
result.status
);
fn fileio_negative_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("fileio_negative.py", "read_file", Cap::FILE_IO, 18));
}
#[test]
fn fileio_unsupported_is_unsupported() {
let path = fixture_path("fileio_unsupported.py");
let mut d = make_diag(&path, "read_config", Cap::FILE_IO, 7);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn fileio_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"fileio_unsupported.py",
"read_config",
Cap::FILE_IO,
7,
));
}
#[test]
fn fileio_adversarial_is_inconclusive_collision() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("fileio_adversarial.py", "read_file", Cap::FILE_IO, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn fileio_adversarial_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("fileio_adversarial.py", "read_file", Cap::FILE_IO, 999));
}
// ── SSRF fixtures ────────────────────────────────────────────────────────
// ── SSRF ─────────────────────────────────────────────────────────────────
#[test]
fn ssrf_positive_is_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("ssrf_positive.py", "fetch_url", Cap::SSRF, 11);
assert_eq!(
result.status, VerifyStatus::Confirmed,
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
result.status, result.detail
);
fn ssrf_positive_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("ssrf_positive.py", "fetch_url", Cap::SSRF, 11));
}
#[test]
fn ssrf_negative_is_not_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("ssrf_negative.py", "fetch_url", Cap::SSRF, 26);
// Blocked by host validation — oracle won't fire.
assert_eq!(
result.status, VerifyStatus::NotConfirmed,
"ssrf_negative must be NotConfirmed; got {:?}",
result.status
);
fn ssrf_negative_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("ssrf_negative.py", "fetch_url", Cap::SSRF, 26));
}
#[test]
fn ssrf_unsupported_is_unsupported() {
let path = fixture_path("ssrf_unsupported.py");
let mut d = make_diag(&path, "fetch", Cap::SSRF, 9);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn ssrf_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec("ssrf_unsupported.py", "fetch", Cap::SSRF, 9));
}
#[test]
fn ssrf_adversarial_is_inconclusive_collision() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("ssrf_adversarial.py", "fetch_url", Cap::SSRF, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn ssrf_adversarial_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec("ssrf_adversarial.py", "fetch_url", Cap::SSRF, 999));
}
// ── XSS fixtures ─────────────────────────────────────────────────────────
// ── XSS ──────────────────────────────────────────────────────────────────
#[test]
fn xss_positive_is_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("xss_positive.py", "render_comment", Cap::HTML_ESCAPE, 9);
assert_eq!(
result.status, VerifyStatus::Confirmed,
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
result.status, result.detail
);
fn xss_positive_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec(
"xss_positive.py",
"render_comment",
Cap::HTML_ESCAPE,
9,
));
}
#[test]
fn xss_negative_is_not_confirmed() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("xss_negative.py", "render_comment", Cap::HTML_ESCAPE, 11);
assert_eq!(
result.status, VerifyStatus::NotConfirmed,
"xss_negative must be NotConfirmed; got {:?}",
result.status
);
fn xss_negative_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec(
"xss_negative.py",
"render_comment",
Cap::HTML_ESCAPE,
11,
));
}
#[test]
fn xss_unsupported_is_unsupported() {
let path = fixture_path("xss_unsupported.py");
let mut d = make_diag(&path, "render", Cap::HTML_ESCAPE, 7);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn xss_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"xss_unsupported.py",
"render",
Cap::HTML_ESCAPE,
7,
));
}
#[test]
fn xss_adversarial_is_inconclusive_collision() {
if !python3_available() {
eprintln!("SKIP: python3 not available");
return;
}
let result = run_fixture("xss_adversarial.py", "render_comment", Cap::HTML_ESCAPE, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn xss_adversarial_matches_golden() {
if !python3_available() { eprintln!("SKIP: python3 not available"); return; }
run_fixture_and_compare_to_golden(&spec(
"xss_adversarial.py",
"render_comment",
Cap::HTML_ESCAPE,
999,
));
}
// ── Secrets fixture ───────────────────────────────────────────────────────
// ── Cross-cutting tests retained verbatim ────────────────────────────────
/// Telemetry must not contain literal secret strings from the fixture.
/// Independent of the golden contract: it inspects the side-channel.
#[test]
fn secret_not_in_telemetry_after_verify() {
if !python3_available() {
@ -377,7 +241,9 @@ mod python_fixture_tests {
return;
}
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _guard = crate::common::fixture_harness::FIXTURE_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let tmp = TempDir::new().unwrap();
let telemetry_path = tmp.path().join("events.jsonl");
@ -391,15 +257,12 @@ mod python_fixture_tests {
let tmp_fix = tmp.path().join("sqli_positive.py");
let _ = std::fs::copy(&fixture, &tmp_fix);
// No set_current_dir: entry file is absolute, copy_entry_file resolves it directly.
let diag = make_diag(&tmp_fix, "login", Cap::SQL_QUERY, 17);
let opts = VerifyOptions::default();
let _ = verify_finding(&diag, &opts);
// Check telemetry doesn't contain any secret patterns.
if telemetry_path.exists() {
let content = std::fs::read_to_string(&telemetry_path).unwrap_or_default();
// Telemetry must not contain the fake AWS key.
assert!(
!content.contains("AKIAFAKETEST00000000"),
"telemetry must not contain fake AWS key; got: {content}"
@ -412,15 +275,11 @@ mod python_fixture_tests {
}
}
// ── Mount-filter gate ─────────────────────────────────────────────────────
/// If the entry file itself matches a sensitive-file pattern (e.g. `id_rsa*`),
/// verify_finding must return Unsupported(RequiredFileRedactedForSecrets).
/// No Python3 needed — the check fires before harness execution.
/// Sensitive-filename gate fires before any harness execution; no
/// python3 needed.
#[test]
fn sensitive_entry_file_is_unsupported() {
let tmp = TempDir::new().unwrap();
// "id_rsa.py" matches the id_rsa* sensitive pattern in mount_filter.
let entry = tmp.path().join("id_rsa.py");
std::fs::write(&entry, "def run(x): pass\n").unwrap();
@ -428,12 +287,7 @@ mod python_fixture_tests {
let opts = VerifyOptions::default();
let result = verify_finding(&diag, &opts);
assert_eq!(
result.status,
VerifyStatus::Unsupported,
"sensitive entry file must be Unsupported; got {:?}",
result.status
);
assert_eq!(result.status, VerifyStatus::Unsupported);
match &result.reason {
Some(UnsupportedReason::RequiredFileRedactedForSecrets(_)) => {}
other => panic!("expected RequiredFileRedactedForSecrets, got {other:?}"),

View file

@ -1,397 +1,225 @@
//! Rust fixture integration tests (Phase 04 acceptance gate).
//!
//! Runs the dynamic verification pipeline against each Rust fixture and
//! asserts the expected verdict. Requires `--features dynamic` and a
//! working `cargo` toolchain on PATH.
//! Each fixture is run through the dynamic verification pipeline; its
//! verdict is then compared against the per-fixture golden under
//! `tests/dynamic_fixtures/rust/{name}.golden.json`. Refresh the goldens
//! via `NYX_UPDATE_GOLDENS=1 ./scripts/update_dynamic_goldens.sh`.
//!
//! Fixture entry points follow the convention:
//! `pub fn run(payload: &str)` in `tests/dynamic_fixtures/rust/{name}.rs`
//!
//! The harness emitter wraps each fixture in a generated `src/main.rs` that
//! reads `NYX_PAYLOAD` from the environment and calls `entry::run(&payload)`.
//!
//! Build note: the first run per capability compiles a Cargo project; subsequent
//! runs with differing entry files hit the build cache only when Cargo.toml and
//! src/entry.rs are identical (the cache key includes the entry file hash).
//! Expect 2-4 compilations per full test run (one per unique dependency set).
//!
//! Run with: `cargo nextest run --features dynamic --test rust_fixtures`
//! Run with: `cargo nextest run --features dynamic --test rust_fixtures`.
mod common;
#[cfg(feature = "dynamic")]
mod rust_fixture_tests {
use crate::common::fixture_harness::{
run_fixture_and_compare_to_golden, CopyStrategy, FixtureSpec,
};
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
use nyx_scanner::evidence::{
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
VerifyStatus,
Confidence, Evidence, FlowStep, FlowStepKind,
};
use nyx_scanner::labels::Cap;
use nyx_scanner::patterns::{FindingCategory, Severity};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::TempDir;
// Serialize all fixture tests: prevents races on process-global env vars
// (NYX_REPRO_BASE, NYX_TELEMETRY_PATH) and the shared build cache dir.
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/dynamic_fixtures/rust")
.join(name)
fn spec(fixture: &'static str, func: &'static str, cap: Cap, sink_line: u32) -> FixtureSpec<'static> {
FixtureSpec {
lang_dir: "rust",
fixture,
func,
cap,
sink_line,
confidence: Confidence::High,
copy: CopyStrategy::RustEntry,
}
}
/// Run a Rust fixture through the full dynamic verification pipeline.
///
/// The fixture file is copied to a temp dir as `src/entry.rs`.
/// `NYX_REPRO_BASE` and `NYX_TELEMETRY_PATH` are redirected to temp dirs.
fn run_fixture(
fixture: &str,
func: &str,
fn low_spec(
fixture: &'static str,
func: &'static str,
cap: Cap,
sink_line: u32,
) -> nyx_scanner::evidence::VerifyResult {
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = fixture_path(fixture);
let tmp = TempDir::new().unwrap();
// Rust fixtures live at src/entry.rs inside the harness workdir;
// the Diag's entry_file points to the fixture source on disk.
let dst_dir = tmp.path().join("src");
std::fs::create_dir_all(&dst_dir).unwrap();
let dst = dst_dir.join("entry.rs");
std::fs::copy(&path, &dst).expect("fixture file must exist");
unsafe {
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
std::env::set_var(
"NYX_TELEMETRY_PATH",
tmp.path().join("events.jsonl").to_str().unwrap(),
);
) -> FixtureSpec<'static> {
FixtureSpec {
lang_dir: "rust",
fixture,
func,
cap,
sink_line,
confidence: Confidence::Low,
copy: CopyStrategy::RustEntry,
}
// Point the Diag at the original fixture path (absolute), not the copy.
// The harness emitter reads the file at entry_file to extract source.
let diag = make_diag(&path, func, cap, sink_line);
let opts = VerifyOptions::default();
let result = verify_finding(&diag, &opts);
unsafe {
std::env::remove_var("NYX_REPRO_BASE");
std::env::remove_var("NYX_TELEMETRY_PATH");
}
result
}
// ── SQLi fixtures ────────────────────────────────────────────────────────
// ── SQLi ─────────────────────────────────────────────────────────────────
#[test]
fn sqli_positive_is_confirmed() {
let result = run_fixture("sqli_positive.rs", "run", Cap::SQL_QUERY, 18);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
assert!(
result.triggered_payload.is_some(),
"Confirmed result must have triggered_payload"
);
fn sqli_positive_matches_golden() {
run_fixture_and_compare_to_golden(&spec("sqli_positive.rs", "run", Cap::SQL_QUERY, 18));
}
#[test]
fn sqli_negative_is_not_confirmed() {
let result = run_fixture("sqli_negative.rs", "run", Cap::SQL_QUERY, 22);
assert_eq!(
result.status,
VerifyStatus::NotConfirmed,
"sqli_negative must be NotConfirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn sqli_negative_matches_golden() {
run_fixture_and_compare_to_golden(&spec("sqli_negative.rs", "run", Cap::SQL_QUERY, 22));
}
#[test]
fn sqli_unsupported_is_unsupported() {
let path = fixture_path("sqli_unsupported.rs");
let mut d = make_diag(&path, "find_user", Cap::SQL_QUERY, 10);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn sqli_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"sqli_unsupported.rs",
"find_user",
Cap::SQL_QUERY,
10,
));
}
#[test]
fn sqli_adversarial_is_inconclusive_collision() {
// Adversarial prints oracle marker without __NYX_SINK_HIT__:
// oracle_fired = true, sink_hit = false → OracleCollisionSuspected.
let result = run_fixture("sqli_adversarial.rs", "run", Cap::SQL_QUERY, 999);
assert_eq!(
result.status,
VerifyStatus::Inconclusive,
"sqli_adversarial must be Inconclusive; got {:?}",
result.status
);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected),
"adversarial must be OracleCollisionSuspected"
);
fn sqli_adversarial_matches_golden() {
run_fixture_and_compare_to_golden(&spec("sqli_adversarial.rs", "run", Cap::SQL_QUERY, 999));
}
// ── Command injection fixtures ───────────────────────────────────────────
// ── Command injection ────────────────────────────────────────────────────
#[test]
fn cmdi_positive_is_confirmed() {
let result = run_fixture("cmdi_positive.rs", "run", Cap::CODE_EXEC, 17);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn cmdi_positive_matches_golden() {
run_fixture_and_compare_to_golden(&spec("cmdi_positive.rs", "run", Cap::CODE_EXEC, 17));
}
#[test]
fn cmdi_negative_is_not_confirmed() {
let result = run_fixture("cmdi_negative.rs", "run", Cap::CODE_EXEC, 17);
assert_eq!(
result.status,
VerifyStatus::NotConfirmed,
"cmdi_negative must be NotConfirmed; got {:?}",
result.status
);
fn cmdi_negative_matches_golden() {
run_fixture_and_compare_to_golden(&spec("cmdi_negative.rs", "run", Cap::CODE_EXEC, 17));
}
#[test]
fn cmdi_unsupported_is_unsupported() {
let path = fixture_path("cmdi_unsupported.rs");
let mut d = make_diag(&path, "execute", Cap::CODE_EXEC, 9);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn cmdi_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"cmdi_unsupported.rs",
"execute",
Cap::CODE_EXEC,
9,
));
}
#[test]
fn cmdi_adversarial_is_inconclusive_collision() {
let result = run_fixture("cmdi_adversarial.rs", "run", Cap::CODE_EXEC, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn cmdi_adversarial_matches_golden() {
run_fixture_and_compare_to_golden(&spec("cmdi_adversarial.rs", "run", Cap::CODE_EXEC, 999));
}
// ── File I/O fixtures ────────────────────────────────────────────────────
// ── File I/O ─────────────────────────────────────────────────────────────
#[test]
fn fileio_positive_is_confirmed() {
let result = run_fixture("fileio_positive.rs", "run", Cap::FILE_IO, 7);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn fileio_positive_matches_golden() {
run_fixture_and_compare_to_golden(&spec("fileio_positive.rs", "run", Cap::FILE_IO, 7));
}
#[test]
fn fileio_negative_is_not_confirmed() {
let result = run_fixture("fileio_negative.rs", "run", Cap::FILE_IO, 17);
assert_eq!(
result.status,
VerifyStatus::NotConfirmed,
"fileio_negative must be NotConfirmed; got {:?}",
result.status
);
fn fileio_negative_matches_golden() {
run_fixture_and_compare_to_golden(&spec("fileio_negative.rs", "run", Cap::FILE_IO, 17));
}
#[test]
fn fileio_unsupported_is_unsupported() {
let path = fixture_path("fileio_unsupported.rs");
let mut d = make_diag(&path, "read", Cap::FILE_IO, 8);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn fileio_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"fileio_unsupported.rs",
"read",
Cap::FILE_IO,
8,
));
}
#[test]
fn fileio_adversarial_is_inconclusive_collision() {
let result = run_fixture("fileio_adversarial.rs", "run", Cap::FILE_IO, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn fileio_adversarial_matches_golden() {
run_fixture_and_compare_to_golden(&spec("fileio_adversarial.rs", "run", Cap::FILE_IO, 999));
}
// ── SSRF fixtures ────────────────────────────────────────────────────────
// ── SSRF ─────────────────────────────────────────────────────────────────
#[test]
fn ssrf_positive_is_confirmed() {
let result = run_fixture("ssrf_positive.rs", "run", Cap::SSRF, 7);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn ssrf_positive_matches_golden() {
run_fixture_and_compare_to_golden(&spec("ssrf_positive.rs", "run", Cap::SSRF, 7));
}
#[test]
fn ssrf_negative_is_not_confirmed() {
let result = run_fixture("ssrf_negative.rs", "run", Cap::SSRF, 13);
assert_eq!(
result.status,
VerifyStatus::NotConfirmed,
"ssrf_negative must be NotConfirmed; got {:?}",
result.status
);
fn ssrf_negative_matches_golden() {
run_fixture_and_compare_to_golden(&spec("ssrf_negative.rs", "run", Cap::SSRF, 13));
}
#[test]
fn ssrf_unsupported_is_unsupported() {
let path = fixture_path("ssrf_unsupported.rs");
let mut d = make_diag(&path, "get", Cap::SSRF, 8);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn ssrf_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec("ssrf_unsupported.rs", "get", Cap::SSRF, 8));
}
#[test]
fn ssrf_adversarial_is_inconclusive_collision() {
let result = run_fixture("ssrf_adversarial.rs", "run", Cap::SSRF, 999);
assert_eq!(result.status, VerifyStatus::Inconclusive);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected)
);
fn ssrf_adversarial_matches_golden() {
run_fixture_and_compare_to_golden(&spec("ssrf_adversarial.rs", "run", Cap::SSRF, 999));
}
// ── XSS fixtures ─────────────────────────────────────────────────────────
// ── XSS ──────────────────────────────────────────────────────────────────
#[test]
fn xss_positive_is_confirmed() {
let result = run_fixture("xss_positive.rs", "run", Cap::HTML_ESCAPE, 11);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
assert!(
result.triggered_payload.is_some(),
"Confirmed result must have triggered_payload"
);
fn xss_positive_matches_golden() {
run_fixture_and_compare_to_golden(&spec("xss_positive.rs", "run", Cap::HTML_ESCAPE, 11));
}
#[test]
fn xss_negative_is_not_confirmed() {
let result = run_fixture("xss_negative.rs", "run", Cap::HTML_ESCAPE, 15);
assert_eq!(
result.status,
VerifyStatus::NotConfirmed,
"xss_negative must be NotConfirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn xss_negative_matches_golden() {
run_fixture_and_compare_to_golden(&spec("xss_negative.rs", "run", Cap::HTML_ESCAPE, 15));
}
#[test]
fn xss_unsupported_is_unsupported() {
let path = fixture_path("xss_unsupported.rs");
let mut d = make_diag(&path, "render", Cap::HTML_ESCAPE, 14);
d.confidence = Some(Confidence::Low);
let opts = VerifyOptions::default();
let result = verify_finding(&d, &opts);
assert_eq!(result.status, VerifyStatus::Unsupported);
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
fn xss_unsupported_matches_golden() {
run_fixture_and_compare_to_golden(&low_spec(
"xss_unsupported.rs",
"render",
Cap::HTML_ESCAPE,
14,
));
}
#[test]
fn xss_adversarial_is_inconclusive_collision() {
let result = run_fixture("xss_adversarial.rs", "run", Cap::HTML_ESCAPE, 999);
assert_eq!(
result.status,
VerifyStatus::Inconclusive,
"xss_adversarial must be Inconclusive; got {:?}",
result.status
);
assert_eq!(
result.inconclusive_reason,
Some(InconclusiveReason::OracleCollisionSuspected),
"adversarial must be OracleCollisionSuspected"
);
fn xss_adversarial_matches_golden() {
run_fixture_and_compare_to_golden(&spec(
"xss_adversarial.rs",
"run",
Cap::HTML_ESCAPE,
999,
));
}
// ── Variant fixtures (smoke-test second positive paths) ──────────────────
// ── Smoke-test second positive paths ─────────────────────────────────────
#[test]
fn cmdi_positive2_is_confirmed() {
let result = run_fixture("cmdi_positive2.rs", "run", Cap::CODE_EXEC, 17);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"cmdi_positive2 must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn cmdi_positive2_matches_golden() {
run_fixture_and_compare_to_golden(&spec("cmdi_positive2.rs", "run", Cap::CODE_EXEC, 17));
}
#[test]
fn fileio_positive2_is_confirmed() {
let result = run_fixture("fileio_positive2.rs", "run", Cap::FILE_IO, 11);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"fileio_positive2 must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn fileio_positive2_matches_golden() {
run_fixture_and_compare_to_golden(&spec("fileio_positive2.rs", "run", Cap::FILE_IO, 11));
}
#[test]
fn ssrf_positive2_is_confirmed() {
let result = run_fixture("ssrf_positive2.rs", "run", Cap::SSRF, 7);
assert_eq!(
result.status,
VerifyStatus::Confirmed,
"ssrf_positive2 must be Confirmed; got {:?} (detail: {:?})",
result.status,
result.detail
);
fn ssrf_positive2_matches_golden() {
run_fixture_and_compare_to_golden(&spec("ssrf_positive2.rs", "run", Cap::SSRF, 7));
}
// ── Harness architecture: non-Python-specific gate ───────────────────────
// ── Pipeline non-panic gate ──────────────────────────────────────────────
/// Rust fixture must produce a VerifyResult (not panic or ICE).
/// This is the Phase 04 acceptance gate: the dynamic pipeline handles
/// a compiled-language finding without Python-specific assumptions.
/// Confirms the Rust pipeline produces a VerifyResult (not a panic/ICE).
/// Independent of the golden contract: this is a structural assertion.
#[test]
fn rust_pipeline_does_not_panic() {
let result = run_fixture("sqli_positive.rs", "run", Cap::SQL_QUERY, 18);
// Any verdict is acceptable; the test asserts non-panic only.
let _ = result;
let _guard = crate::common::fixture_harness::FIXTURE_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/dynamic_fixtures/rust/sqli_positive.rs");
let diag = make_diag(&path, "run", Cap::SQL_QUERY, 18);
let opts = VerifyOptions::default();
let _ = verify_finding(&diag, &opts);
}
// ── Helpers ─────────────────────────────────────────────────────────────
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
let path_str = path.to_string_lossy().into_owned();
let evidence = Evidence {