nyx/tests/symex_switch_tests.rs
Eli Peter a438886217
Python fp and docs updtes (#58)
* refactor: Update comments for clarity and add expectations.json files for performance metrics

* feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks

* feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks

* refactor: Simplify code formatting for better readability in multiple files

* refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration

* feat: Update Java and Python patterns to include new security rules

* refactor: Improve comment clarity and consistency across multiple Rust files

* refactor: Simplify code formatting for improved readability in integration tests and module files

* refactor: Improve comment formatting and enhance clarity in assertions across multiple files
2026-04-29 19:53:34 -04:00

108 lines
3.9 KiB
Rust

//! Symex switch / match per-case path-constraint coverage.
//!
//! Each fixture is scanned in isolation (single-file copy to a tempdir to
//! prevent the language harness from picking up siblings of a different
//! language). The fixtures exercise the per-case fork in
//! `src/symex/executor.rs::step_switch` plus the synthesized
//! `<scrutinee> == <case_literal>` condition wired by
//! `src/cfg/blocks.rs::build_switch` for Rust match, Go switch, and Java
//! arrow-switch.
mod common;
use common::test_config;
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::utils::config::AnalysisMode;
use std::path::{Path, PathBuf};
fn fixture_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("symex")
.join(name)
}
fn scan_isolated(fixture: &Path) -> Vec<Diag> {
let tmp = tempfile::TempDir::with_prefix("nyx_symex_switch_").expect("tempdir");
let dest = tmp.path().join(fixture.file_name().unwrap());
std::fs::copy(fixture, &dest).expect("copy fixture");
let cfg = test_config(AnalysisMode::Full);
nyx_scanner::scan_no_index(tmp.path(), &cfg).expect("scan_no_index should succeed")
}
fn count_relevant(diags: &[Diag]) -> usize {
diags
.iter()
.filter(|d| {
let id = d.id.as_str();
id.starts_with("taint-")
|| id.contains(".sqli.")
|| id.contains(".cmdi.")
|| id.contains(".xss.")
|| id.contains(".ssrf.")
|| id == "cfg-unguarded-sink"
})
.count()
}
/// All three fixtures exercise the same shape: an unsanitized arm that
/// must report at least one finding from a taint/AST sink-pattern rule,
/// and a sanitized arm whose findings (if any) are not promoted to a
/// hard regression. The exact finding count is left loose because
/// per-case suppression precision depends on whether the constraint
/// solver can refine the scrutinee (integer literals do, enum paths
/// do not, see `match_suppresses_safe_arm.rs`).
fn assert_at_least_one_finding(diags: &[Diag], label: &str) {
let n = count_relevant(diags);
assert!(
n >= 1,
"[{label}] expected ≥1 relevant finding (raw arm), got {n}.\n diags = {:#?}",
diags
.iter()
.map(|d| format!("{}:{} {}", d.path, d.line, d.id))
.collect::<Vec<_>>()
);
}
#[test]
fn symex_match_suppresses_safe_arm() {
let path = fixture_path("match_suppresses_safe_arm.rs");
let diags = scan_isolated(&path);
// Rust match arms currently don't reliably surface taint flows from
// the existing engine for this scenario (see
// tests/fixtures/real_world/rust/cfg/match_arms.rs which also only
// emits quality findings, not taint). The acceptance for this
// fixture is therefore: (1) the scan runs to completion without a
// panic, covered by the call to `scan_isolated` returning, and
// (2) at least one finding lands on the Raw arm body (lines
// 22-29). The Safe arm at lines 31-36 must not regress beyond the
// existing baseline.
let raw_arm: Vec<&Diag> = diags
.iter()
.filter(|d| d.path.ends_with("match_suppresses_safe_arm.rs"))
.filter(|d| d.line >= 22 && d.line <= 29)
.collect();
assert!(
!raw_arm.is_empty(),
"[rust_match] expected ≥1 finding on the Raw arm body (lines 22-29), got 0.\n diags = {:#?}",
diags
.iter()
.map(|d| format!("{}:{} {}", d.path, d.line, d.id))
.collect::<Vec<_>>()
);
}
#[test]
fn symex_switch_go() {
let path = fixture_path("switch_go.go");
let diags = scan_isolated(&path);
assert_at_least_one_finding(&diags, "go_switch");
}
#[test]
fn symex_switch_java() {
let path = fixture_path("switch_java.java");
let diags = scan_isolated(&path);
assert_at_least_one_finding(&diags, "java_arrow_switch");
}