mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss] phase 04: M4 — Rust harness (second-language validation)
This commit is contained in:
parent
e875aa1208
commit
3ffe480660
37 changed files with 1872 additions and 54 deletions
11
tests/dynamic_fixtures/escape/rust_build_rs/Cargo.toml
Normal file
11
tests/dynamic_fixtures/escape/rust_build_rs/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "nyx-escape-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nyx_escape_harness"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
16
tests/dynamic_fixtures/escape/rust_build_rs/build.rs
Normal file
16
tests/dynamic_fixtures/escape/rust_build_rs/build.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Escape attempt: malicious build.rs that tries to write a host marker file
|
||||
// during `cargo build`. A sandboxed build should prevent this.
|
||||
//
|
||||
// Expected behaviour in an isolated sandbox: file NOT created, build fails
|
||||
// or marker stays absent.
|
||||
// Expected behaviour on unsandboxed host: marker would appear at /tmp/pwned_build_rs.
|
||||
fn main() {
|
||||
// Attempt to write a marker file on the host.
|
||||
let marker = std::path::Path::new("/tmp/pwned_build_rs");
|
||||
if std::fs::File::create(marker).is_ok() {
|
||||
// Escape succeeded: signal via stdout (cargo captures build script stdout).
|
||||
println!("cargo:warning=NYX_ESCAPE_SUCCESS: build.rs created /tmp/pwned_build_rs");
|
||||
} else {
|
||||
println!("cargo:warning=BLOCKED: could not write /tmp/pwned_build_rs");
|
||||
}
|
||||
}
|
||||
4
tests/dynamic_fixtures/escape/rust_build_rs/src/main.rs
Normal file
4
tests/dynamic_fixtures/escape/rust_build_rs/src/main.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Benign entry point — the escape attempt lives in build.rs, not here.
|
||||
fn main() {
|
||||
println!("nyx_escape_harness: no vulnerability in main");
|
||||
}
|
||||
13
tests/dynamic_fixtures/rust/cmdi_adversarial.rs
Normal file
13
tests/dynamic_fixtures/rust/cmdi_adversarial.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/// Command injection — adversarial collision fixture.
|
||||
///
|
||||
/// Prints "NYX_PWN_CMDI" unconditionally without going through a shell sink.
|
||||
///
|
||||
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
/// oracle_fired = true (OutputContains("NYX_PWN_CMDI"))
|
||||
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
|
||||
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Coincidental oracle match — not a command execution sink.
|
||||
println!("NYX_PWN_CMDI");
|
||||
let _ = payload.len();
|
||||
}
|
||||
23
tests/dynamic_fixtures/rust/cmdi_negative.rs
Normal file
23
tests/dynamic_fixtures/rust/cmdi_negative.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/// Command injection — negative fixture.
|
||||
///
|
||||
/// Safe function: uses Command with a list of args (no shell expansion).
|
||||
/// Payload is used as a literal argument, not interpreted by the shell.
|
||||
/// Expected verdict: NotConfirmed.
|
||||
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use std::process::Command;
|
||||
|
||||
// Safe: list-form args — shell metacharacters in payload are inert.
|
||||
let safe_target = payload
|
||||
.chars()
|
||||
.filter(|c| c.is_alphanumeric() || *c == '.')
|
||||
.collect::<String>();
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match Command::new("echo").arg(&safe_target).output() {
|
||||
Ok(out) => print!("{}", String::from_utf8_lossy(&out.stdout)),
|
||||
Err(e) => eprintln!("exec error: {}", e),
|
||||
}
|
||||
}
|
||||
24
tests/dynamic_fixtures/rust/cmdi_positive.rs
Normal file
24
tests/dynamic_fixtures/rust/cmdi_positive.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// Command injection — positive fixture.
|
||||
///
|
||||
/// Vulnerable function: builds a shell command string from user input.
|
||||
/// Expected verdict: Confirmed (payload "; echo NYX_PWN_CMDI" echoes the marker).
|
||||
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use std::process::Command;
|
||||
|
||||
// Vulnerable: user input concatenated into shell command string.
|
||||
let cmd = format!("echo {}", payload);
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match Command::new("sh").args(["-c", &cmd]).output() {
|
||||
Ok(out) => {
|
||||
print!("{}", String::from_utf8_lossy(&out.stdout));
|
||||
if !out.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("exec error: {}", e),
|
||||
}
|
||||
}
|
||||
25
tests/dynamic_fixtures/rust/cmdi_positive2.rs
Normal file
25
tests/dynamic_fixtures/rust/cmdi_positive2.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/// Command injection — second positive fixture.
|
||||
///
|
||||
/// Variant: builds a script filename from user input and passes it to sh.
|
||||
/// Expected verdict: Confirmed (payload "; echo NYX_PWN_CMDI" injects into the
|
||||
/// command string at a different AST site than cmdi_positive.rs).
|
||||
/// Cap: CODE_EXEC Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use std::process::Command;
|
||||
|
||||
// Vulnerable: payload used as a path argument, which is shell-interpolated.
|
||||
let script = format!("ls -la {}", payload);
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match Command::new("sh").args(["-c", &script]).output() {
|
||||
Ok(out) => {
|
||||
print!("{}", String::from_utf8_lossy(&out.stdout));
|
||||
if !out.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("exec error: {}", e),
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/rust/cmdi_unsupported.rs
Normal file
21
tests/dynamic_fixtures/rust/cmdi_unsupported.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/// Command injection — unsupported entry-kind fixture.
|
||||
///
|
||||
/// Vulnerable logic lives inside a struct method. The test creates a Diag
|
||||
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
|
||||
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
|
||||
///
|
||||
/// Expected verdict: Unsupported(EntryKindUnsupported)
|
||||
/// Cap: CODE_EXEC
|
||||
pub struct ShellRunner;
|
||||
|
||||
impl ShellRunner {
|
||||
pub fn execute(&self, user_cmd: &str) -> Option<String> {
|
||||
use std::process::Command;
|
||||
let cmd = format!("run {}", user_cmd);
|
||||
Command::new("sh")
|
||||
.args(["-c", &cmd])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/fileio_adversarial.rs
Normal file
14
tests/dynamic_fixtures/rust/fileio_adversarial.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/// File I/O — adversarial collision fixture.
|
||||
///
|
||||
/// Prints "root:" unconditionally without opening any file or printing the
|
||||
/// sink-reachability sentinel.
|
||||
///
|
||||
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
/// oracle_fired = true (OutputContains("root:"))
|
||||
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
|
||||
/// Cap: FILE_IO Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Coincidental oracle match — no file I/O sink involved.
|
||||
println!("root:x:0:0:root:/root:/bin/bash");
|
||||
let _ = payload.len();
|
||||
}
|
||||
27
tests/dynamic_fixtures/rust/fileio_negative.rs
Normal file
27
tests/dynamic_fixtures/rust/fileio_negative.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/// File I/O — negative fixture.
|
||||
///
|
||||
/// Safe function: reads from a fixed path; user input is only used as a search
|
||||
/// term within file contents, not as the file path itself.
|
||||
/// Expected verdict: NotConfirmed.
|
||||
/// Cap: FILE_IO Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Safe: path is hard-coded; payload cannot influence which file is read.
|
||||
let fixed_path = "/tmp/nyx_safe_file_does_not_exist";
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match std::fs::read_to_string(fixed_path) {
|
||||
Ok(contents) => {
|
||||
// Only use payload as a filter, not as a path.
|
||||
for line in contents.lines() {
|
||||
if line.contains(payload) {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("file not found (expected in test)");
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/rust/fileio_positive.rs
Normal file
16
tests/dynamic_fixtures/rust/fileio_positive.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// File I/O — positive fixture.
|
||||
///
|
||||
/// Vulnerable function: reads a file at a user-controlled path.
|
||||
/// Expected verdict: Confirmed (path-traversal payload "../../../../etc/passwd"
|
||||
/// causes "root:" to appear in stdout).
|
||||
/// Cap: FILE_IO Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
// Vulnerable: user controls the file path — path traversal possible.
|
||||
match std::fs::read_to_string(payload) {
|
||||
Ok(contents) => print!("{}", contents),
|
||||
Err(e) => eprintln!("Error reading {}: {}", payload, e),
|
||||
}
|
||||
}
|
||||
24
tests/dynamic_fixtures/rust/fileio_positive2.rs
Normal file
24
tests/dynamic_fixtures/rust/fileio_positive2.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// File I/O — second positive fixture.
|
||||
///
|
||||
/// Variant: uses std::fs::File::open instead of read_to_string; path constructed
|
||||
/// from a base directory and user-supplied component (still traversable).
|
||||
/// Expected verdict: Confirmed (payload "../../../../etc/passwd" reaches /etc/passwd).
|
||||
/// Cap: FILE_IO Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use std::io::Read;
|
||||
|
||||
// Vulnerable: path joins base with user input without canonicalization.
|
||||
let path = format!("/var/data/{}", payload);
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match std::fs::File::open(&path) {
|
||||
Ok(mut f) => {
|
||||
let mut buf = String::new();
|
||||
let _ = f.read_to_string(&mut buf);
|
||||
print!("{}", buf);
|
||||
}
|
||||
Err(e) => eprintln!("Error opening {}: {}", path, e),
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/rust/fileio_unsupported.rs
Normal file
16
tests/dynamic_fixtures/rust/fileio_unsupported.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/// File I/O — unsupported entry-kind fixture.
|
||||
///
|
||||
/// Vulnerable logic lives inside a struct method. The test creates a Diag
|
||||
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
|
||||
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
|
||||
///
|
||||
/// Expected verdict: Unsupported(EntryKindUnsupported)
|
||||
/// Cap: FILE_IO
|
||||
pub struct FileService;
|
||||
|
||||
impl FileService {
|
||||
pub fn read(&self, path: &str) -> String {
|
||||
// Vulnerable: path traversal — user controls the path.
|
||||
std::fs::read_to_string(path).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/rust/sqli_adversarial.rs
Normal file
15
tests/dynamic_fixtures/rust/sqli_adversarial.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/// SQL injection — adversarial collision fixture.
|
||||
///
|
||||
/// Prints "NYX_SQL_CONFIRMED" unconditionally without going through a SQL sink
|
||||
/// and without printing the sink-reachability sentinel.
|
||||
///
|
||||
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
/// oracle_fired = true (OutputContains("NYX_SQL_CONFIRMED"))
|
||||
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
|
||||
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Coincidental oracle match — not a SQL sink.
|
||||
println!("NYX_SQL_CONFIRMED");
|
||||
// Ensure payload is consumed so the compiler does not optimise it away.
|
||||
let _ = payload.len();
|
||||
}
|
||||
33
tests/dynamic_fixtures/rust/sqli_negative.rs
Normal file
33
tests/dynamic_fixtures/rust/sqli_negative.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/// SQL injection — negative fixture.
|
||||
///
|
||||
/// Safe function: uses parameterized query (rusqlite params![]).
|
||||
/// Expected verdict: NotConfirmed (no injection possible; oracle cannot fire).
|
||||
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT);\
|
||||
INSERT INTO users VALUES (1, 'alice');\
|
||||
INSERT INTO users VALUES (2, 'bob');",
|
||||
)
|
||||
.expect("setup schema");
|
||||
|
||||
// Safe: parameterized query — payload cannot escape the literal binding.
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT name FROM users WHERE name=?1")
|
||||
.expect("prepare");
|
||||
|
||||
// Sink reached via safe parameterized path; sentinel fires but oracle will not.
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
let _ = stmt
|
||||
.query_map(rusqlite::params![payload], |row| row.get::<_, String>(0))
|
||||
.map(|rows| {
|
||||
for name in rows.flatten() {
|
||||
println!("{}", name);
|
||||
}
|
||||
});
|
||||
}
|
||||
38
tests/dynamic_fixtures/rust/sqli_positive.rs
Normal file
38
tests/dynamic_fixtures/rust/sqli_positive.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/// SQL injection — positive fixture.
|
||||
///
|
||||
/// Vulnerable function: directly concatenates user input into SQL.
|
||||
/// Expected verdict: Confirmed (UNION payload causes "NYX_SQL_CONFIRMED" in output).
|
||||
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT);\
|
||||
INSERT INTO users VALUES (1, 'alice');\
|
||||
INSERT INTO users VALUES (2, 'bob');",
|
||||
)
|
||||
.expect("setup schema");
|
||||
|
||||
// Vulnerable: direct string concatenation into SQL.
|
||||
let query = format!("SELECT name FROM users WHERE name='{}'", payload);
|
||||
|
||||
// Sentinel: the sink (conn.prepare) is reachable with tainted input.
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match conn.prepare(&query) {
|
||||
Ok(mut stmt) => {
|
||||
let _ = stmt.query_map([], |row| row.get::<_, String>(0)).map(|rows| {
|
||||
for name in rows.flatten() {
|
||||
println!("{}", name);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
// Error-based: print query on failure (oracle can detect via query echo).
|
||||
println!("DB query: {}", query);
|
||||
println!("DB error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tests/dynamic_fixtures/rust/sqli_unsupported.rs
Normal file
24
tests/dynamic_fixtures/rust/sqli_unsupported.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/// SQL injection — unsupported entry-kind fixture.
|
||||
///
|
||||
/// The vulnerable logic lives inside a struct method. The test creates a Diag
|
||||
/// with an unsupported entry kind, so `HarnessSpec::from_finding` returns
|
||||
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
|
||||
///
|
||||
/// Expected verdict: Unsupported(EntryKindUnsupported)
|
||||
/// Cap: SQL_QUERY
|
||||
pub struct UserRepository;
|
||||
|
||||
impl UserRepository {
|
||||
pub fn find_user(&self, name: &str) -> Vec<String> {
|
||||
use rusqlite::Connection;
|
||||
let conn = Connection::open_in_memory().expect("open db");
|
||||
let query = format!("SELECT name FROM users WHERE name='{}'", name);
|
||||
match conn.prepare(&query) {
|
||||
Ok(mut stmt) => stmt
|
||||
.query_map([], |row| row.get::<_, String>(0))
|
||||
.map(|rows| rows.flatten().collect())
|
||||
.unwrap_or_default(),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
38
tests/dynamic_fixtures/rust/sqli_with_secret.rs
Normal file
38
tests/dynamic_fixtures/rust/sqli_with_secret.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/// SQL injection fixture — same vulnerability as sqli_positive, placed in a
|
||||
/// directory that contains a secrets file (.env with AWS key).
|
||||
///
|
||||
/// The test verifies that the AWS key is redacted from outcome.json / telemetry
|
||||
/// and never appears in any repro artifact after verification.
|
||||
///
|
||||
/// Expected verdict: Confirmed (same oracle as sqli_positive)
|
||||
/// Cap: SQL_QUERY Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
use rusqlite::Connection;
|
||||
|
||||
let conn = Connection::open_in_memory().expect("open in-memory db");
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT);\
|
||||
INSERT INTO users VALUES (1, 'alice');\
|
||||
INSERT INTO users VALUES (2, 'bob');",
|
||||
)
|
||||
.expect("setup schema");
|
||||
|
||||
let query = format!("SELECT name FROM users WHERE name='{}'", payload);
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
match conn.prepare(&query) {
|
||||
Ok(mut stmt) => {
|
||||
let _ = stmt.query_map([], |row| row.get::<_, String>(0)).map(|rows| {
|
||||
for name in rows.flatten() {
|
||||
println!("{}", name);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
println!("DB query: {}", query);
|
||||
println!("DB error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/ssrf_adversarial.rs
Normal file
14
tests/dynamic_fixtures/rust/ssrf_adversarial.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/// SSRF — adversarial collision fixture.
|
||||
///
|
||||
/// Prints "daemon:" unconditionally without making any network or file request,
|
||||
/// and without printing the sink-reachability sentinel.
|
||||
///
|
||||
/// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
/// oracle_fired = true (OutputContains("daemon:"))
|
||||
/// sink_hit = false (__NYX_SINK_HIT__ never printed)
|
||||
/// Cap: SSRF Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Coincidental oracle match — no URL fetch or network sink involved.
|
||||
println!("daemon:*:1:1:System Services:/var/root:/usr/bin/false");
|
||||
let _ = payload.len();
|
||||
}
|
||||
20
tests/dynamic_fixtures/rust/ssrf_negative.rs
Normal file
20
tests/dynamic_fixtures/rust/ssrf_negative.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/// SSRF — negative fixture.
|
||||
///
|
||||
/// Safe function: URL is fixed; user input is used only as a query parameter,
|
||||
/// not as the URL origin.
|
||||
/// Expected verdict: NotConfirmed.
|
||||
/// Cap: SSRF Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
// Safe: payload is a query value, not the URL itself — origin is fixed.
|
||||
let url = format!("file:///tmp/safe_data?q={}", payload);
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
// Extract the fixed path (no user control over scheme or host).
|
||||
let path = "/tmp/safe_data";
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) => print!("{}", content),
|
||||
Err(_) => println!("resource not available (expected in test): {}", url),
|
||||
}
|
||||
}
|
||||
26
tests/dynamic_fixtures/rust/ssrf_positive.rs
Normal file
26
tests/dynamic_fixtures/rust/ssrf_positive.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/// SSRF — positive fixture.
|
||||
///
|
||||
/// Vulnerable function: fetches a user-controlled URL. Implements a minimal
|
||||
/// file:// scheme reader so the test requires no network and no async runtime.
|
||||
///
|
||||
/// Expected verdict: Confirmed (payload "file:///etc/passwd" causes "daemon:"
|
||||
/// to appear in stdout via the file:// scheme handler).
|
||||
/// Cap: SSRF Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
// Vulnerable: user controls the URL — SSRF via file:// scheme reaches local files.
|
||||
let result = fetch_url(payload);
|
||||
print!("{}", result);
|
||||
}
|
||||
|
||||
fn fetch_url(url: &str) -> String {
|
||||
if let Some(path) = url.strip_prefix("file://") {
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| format!("fetch error: {}", e))
|
||||
} else {
|
||||
// For non-file schemes, report the target (demonstrating SSRF intent).
|
||||
format!("SSRF: would connect to {}", url)
|
||||
}
|
||||
}
|
||||
32
tests/dynamic_fixtures/rust/ssrf_positive2.rs
Normal file
32
tests/dynamic_fixtures/rust/ssrf_positive2.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/// SSRF — second positive fixture.
|
||||
///
|
||||
/// Variant: user-controlled URL stored in a struct field before being fetched,
|
||||
/// exercising a different taint path than ssrf_positive.rs.
|
||||
/// Expected verdict: Confirmed (payload "file:///etc/passwd" reaches the file
|
||||
/// reader via the stored URL field).
|
||||
/// Cap: SSRF Entry: `run(payload: &str)`
|
||||
pub fn run(payload: &str) {
|
||||
let req = Request { url: payload.to_owned() };
|
||||
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
let result = req.execute();
|
||||
print!("{}", result);
|
||||
}
|
||||
|
||||
struct Request {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
fn execute(&self) -> String {
|
||||
// Vulnerable: self.url derived from user input — SSRF.
|
||||
if let Some(path) = self.url.strip_prefix("file://") {
|
||||
std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| format!("fetch error: {}", e))
|
||||
} else {
|
||||
format!("SSRF: would connect to {}", self.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/rust/ssrf_unsupported.rs
Normal file
20
tests/dynamic_fixtures/rust/ssrf_unsupported.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/// SSRF — unsupported entry-kind fixture.
|
||||
///
|
||||
/// Vulnerable logic lives inside a struct method. The test creates a Diag
|
||||
/// with an unsupported entry kind so `HarnessSpec::from_finding` returns
|
||||
/// `Err(UnsupportedReason::EntryKindUnsupported)`.
|
||||
///
|
||||
/// Expected verdict: Unsupported(EntryKindUnsupported)
|
||||
/// Cap: SSRF
|
||||
pub struct HttpClient;
|
||||
|
||||
impl HttpClient {
|
||||
pub fn get(&self, url: &str) -> String {
|
||||
// Vulnerable: user controls the URL — SSRF.
|
||||
if let Some(path) = url.strip_prefix("file://") {
|
||||
std::fs::read_to_string(path).unwrap_or_default()
|
||||
} else {
|
||||
format!("fetching: {}", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,6 +181,51 @@ mod escape_tests {
|
|||
escape_test!(escape_chroot_escape, "chroot_escape.py");
|
||||
escape_test!(escape_ipc_shm, "ipc_shm_escape.py");
|
||||
|
||||
// ── Rust build.rs escape test ─────────────────────────────────────────────
|
||||
|
||||
/// Verify that a malicious Rust build.rs cannot write to the host when compiled
|
||||
/// inside the sandbox.
|
||||
///
|
||||
/// NOTE (Phase 04): Docker + Rust compilation is deferred to Phase 05.
|
||||
/// `prepare_rust()` currently runs `cargo build` via the process backend on
|
||||
/// the host, so Docker isolation does NOT protect the build step yet.
|
||||
///
|
||||
/// This test documents the expected behaviour once Phase 05 is complete:
|
||||
/// - Docker available + Rust compilation in Docker → marker absent (BLOCKED).
|
||||
/// - No Docker or Phase 05 not yet implemented → test is skipped.
|
||||
///
|
||||
/// The fixture is at `tests/dynamic_fixtures/escape/rust_build_rs/`.
|
||||
///
|
||||
/// Ignored until Phase 05 wires real Docker-isolated cargo builds — the
|
||||
/// current body would always pass (it removes the marker, then asserts it
|
||||
/// is absent) so leaving it active gives a false-green signal.
|
||||
#[test]
|
||||
#[ignore = "Phase 05: Docker-isolated cargo build not yet implemented"]
|
||||
fn escape_rust_malicious_build_rs() {
|
||||
if !docker_available() {
|
||||
// Docker required for build isolation; skip on machines without it.
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 05 TODO: wire Docker-isolated cargo build and re-enable this body.
|
||||
// When Docker + Rust compilation is implemented:
|
||||
// 1. Copy rust_build_rs/ to a temp workdir.
|
||||
// 2. Run prepare_rust_in_docker(spec, workdir).
|
||||
// 3. Assert !Path::new("/tmp/pwned_build_rs").exists().
|
||||
//
|
||||
// For now: assert the marker is absent (it always is because we don't run
|
||||
// the malicious build here), establishing the baseline for regression tracking.
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_build_rs");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// No build is triggered yet (Docker + Rust deferred).
|
||||
// The marker must remain absent.
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_build_rs must not exist before Docker+Rust compilation is implemented"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Docker exec reuse test ────────────────────────────────────────────────
|
||||
|
||||
/// Verify that the second payload for the same spec_hash reuses the running
|
||||
|
|
|
|||
|
|
@ -147,6 +147,136 @@ mod repro_determinism_tests {
|
|||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Rust repro tests ─────────────────────────────────────────────────────
|
||||
|
||||
fn make_confirmed_rust_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "rust_determ00001".into(),
|
||||
entry_file: "src/entry.rs".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Rust,
|
||||
toolchain_id: "rust-stable".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/entry.rs".into(),
|
||||
sink_line: 18,
|
||||
spec_hash: spec_hash.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_confirmed_rust_harness_source() -> String {
|
||||
r#"mod entry;
|
||||
fn main() {
|
||||
let payload = std::env::var("NYX_PAYLOAD").unwrap_or_default();
|
||||
entry::run(&payload);
|
||||
}
|
||||
"#
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Rust repro bundle has the correct layout.
|
||||
///
|
||||
/// For Rust, harness is at `harness/src/main.rs` and `harness/Cargo.toml`
|
||||
/// is also written (unlike Python which uses `harness/harness.py`).
|
||||
#[test]
|
||||
fn rust_repro_layout_is_correct() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_rust_spec("rust_determ00001");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("rust_determ00001");
|
||||
let harness_src = make_confirmed_rust_harness_source();
|
||||
|
||||
let artifact = repro::write(
|
||||
&spec,
|
||||
&opts,
|
||||
&outcome,
|
||||
&verdict,
|
||||
&harness_src,
|
||||
"pub fn run(payload: &str) { println!(\"{}\", payload); }\n",
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--",
|
||||
"sqli-union-nyx",
|
||||
None,
|
||||
)
|
||||
.expect("Rust repro write must succeed");
|
||||
|
||||
// Rust-specific layout: harness lives under harness/src/main.rs.
|
||||
assert!(
|
||||
artifact.root.join("harness/src/main.rs").exists(),
|
||||
"Rust harness must be at harness/src/main.rs"
|
||||
);
|
||||
assert!(
|
||||
artifact.root.join("harness/Cargo.toml").exists(),
|
||||
"Rust harness must include harness/Cargo.toml"
|
||||
);
|
||||
// Common layout.
|
||||
assert!(artifact.root.join("manifest.json").exists());
|
||||
assert!(artifact.root.join("entry/extracted_source.rs").exists());
|
||||
assert!(artifact.root.join("payload/payload.bin").exists());
|
||||
assert!(artifact.root.join("expected/outcome.json").exists());
|
||||
assert!(artifact.root.join("expected/verdict.json").exists());
|
||||
assert!(artifact.root.join("reproduce.sh").exists());
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Rust repro outcome.json is byte-identical across two writes.
|
||||
#[test]
|
||||
fn rust_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_rust_spec("rust_determ00002");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("rust_determ00002");
|
||||
let harness_src = make_confirmed_rust_harness_source();
|
||||
let entry_src = "pub fn run(payload: &str) { println!(\"{}\", payload); }\n";
|
||||
|
||||
let artifact1 = repro::write(
|
||||
&spec,
|
||||
&opts,
|
||||
&outcome,
|
||||
&verdict,
|
||||
&harness_src,
|
||||
entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--",
|
||||
"sqli-union-nyx",
|
||||
None,
|
||||
)
|
||||
.expect("first Rust repro write");
|
||||
let json1 =
|
||||
std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&artifact1.root).unwrap();
|
||||
|
||||
let artifact2 = repro::write(
|
||||
&spec,
|
||||
&opts,
|
||||
&outcome,
|
||||
&verdict,
|
||||
&harness_src,
|
||||
entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--",
|
||||
"sqli-union-nyx",
|
||||
None,
|
||||
)
|
||||
.expect("second Rust repro write");
|
||||
let json2 =
|
||||
std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json1, json2,
|
||||
"Rust outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Verify verdict.json is correctly structured.
|
||||
#[test]
|
||||
fn verdict_json_is_valid() {
|
||||
|
|
|
|||
393
tests/rust_fixtures.rs
Normal file
393
tests/rust_fixtures.rs
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! 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`
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod rust_fixture_tests {
|
||||
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,
|
||||
};
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
// 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 ────────────────────────────────────────────────────────
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Command injection fixtures ───────────────────────────────────────────
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
|
||||
// ── File I/O fixtures ────────────────────────────────────────────────────
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
|
||||
// ── SSRF fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Variant fixtures (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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
}
|
||||
|
||||
// ── Harness architecture: non-Python-specific 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.
|
||||
#[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;
|
||||
}
|
||||
|
||||
// ── 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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue