mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
[pitboss] phase 16: Track B — Rust + C + C++ harness emitter shapes
This commit is contained in:
parent
bf62ae6b9f
commit
76087f931a
31 changed files with 1969 additions and 100 deletions
157
tests/c_fixtures.rs
Normal file
157
tests/c_fixtures.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! C fixture integration tests (Phase 16 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each C shape fixture and
|
||||
//! asserts the expected verdict. Requires `--features dynamic` and `cc` on
|
||||
//! PATH (override via `NYX_CC_BIN`).
|
||||
//!
|
||||
//! File layout per shape:
|
||||
//! ```text
|
||||
//! tests/dynamic_fixtures/c/<shape>/{vuln,benign}.c
|
||||
//! ```
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test c_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod c_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn cc_available() -> bool {
|
||||
let bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
std::process::Command::new(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::C, "c", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── main_argv ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn main_argv_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "vuln.c", "nyx_entry_main", Cap::CODE_EXEC, 23,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_argv_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "benign.c", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "vuln.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 16,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "benign.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
// ── free_fn ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn free_fn_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "vuln.c", "run", Cap::CODE_EXEC, 15,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("free_fn", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_fn_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "benign.c", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("free_fn", &r);
|
||||
}
|
||||
}
|
||||
157
tests/cpp_fixtures.rs
Normal file
157
tests/cpp_fixtures.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! C++ fixture integration tests (Phase 16 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each C++ shape fixture
|
||||
//! and asserts the expected verdict. Requires `--features dynamic` and
|
||||
//! `c++` on PATH (override via `NYX_CXX_BIN`).
|
||||
//!
|
||||
//! File layout per shape:
|
||||
//! ```text
|
||||
//! tests/dynamic_fixtures/cpp/<shape>/{vuln,benign}.cpp
|
||||
//! ```
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test cpp_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod cpp_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn cxx_available() -> bool {
|
||||
let bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned());
|
||||
std::process::Command::new(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Cpp, "cpp", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── main_argv ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn main_argv_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "vuln.cpp", "nyx_entry_main", Cap::CODE_EXEC, 16,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_argv_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "benign.cpp", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "vuln.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 15,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "benign.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
// ── free_fn ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn free_fn_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "vuln.cpp", "run", Cap::CODE_EXEC, 12,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("free_fn", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_fn_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "benign.cpp", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("free_fn", &r);
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)payload; (void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
}
|
||||
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
*
|
||||
* Cap: CODE_EXEC. Concatenates payload into a shell command.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", payload);
|
||||
system(cmd);
|
||||
}
|
||||
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* Phase 16 — libFuzzer entry, benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* Phase 16 — libFuzzer entry, vulnerable.
|
||||
*
|
||||
* Real libFuzzer entry: `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`.
|
||||
* Cap: CODE_EXEC.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %.*s", (int)size, (const char*)data);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* Phase 16 — main(argc, argv), benign.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
* Echoes a fixed greeting; argv is ignored.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Phase 16 — main(argc, argv), vulnerable.
|
||||
*
|
||||
* Entry: nyx_entry_main(int argc, char *argv[])
|
||||
*
|
||||
* Renamed away from `main` so the harness `main` symbol does not collide
|
||||
* when the entry source is `#include`d. The harness emitter recognises the
|
||||
* shape via the `int main(int argc, char *argv[])` substring in the
|
||||
* comment header below, then calls `nyx_entry_main` with payload-bearing
|
||||
* argv. Cap: CODE_EXEC.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", argv[argc - 1]);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 16 — free function with (const char *, size_t), benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
(void)payload; (void)len;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
}
|
||||
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 16 — libFuzzer entry, benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 16 — libFuzzer entry, vulnerable. Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
std::string payload(reinterpret_cast<const char*>(data), size);
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 16 — main(argc, argv), benign.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 16 — main(argc, argv), vulnerable.
|
||||
//
|
||||
// Renamed away from `main` so the harness `main` symbol does not collide.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
std::string cmd = std::string("echo hello ") + argv[argc - 1];
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
16
tests/dynamic_fixtures/rust/actix_route/benign.rs
Normal file
16
tests/dynamic_fixtures/rust/actix_route/benign.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//! Phase 16 — actix_web route, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use actix_web::HttpResponse;`
|
||||
//! Echoes a fixed greeting; payload is dropped on the floor.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(_payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
21
tests/dynamic_fixtures/rust/actix_route/vuln.rs
Normal file
21
tests/dynamic_fixtures/rust/actix_route/vuln.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! Phase 16 — actix_web route, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use actix_web::HttpResponse;`
|
||||
//! The fixture exposes a synchronous shim with the same conceptual entry
|
||||
//! signature so the harness build does not need to link real actix_web.
|
||||
//! Cap: CODE_EXEC
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
15
tests/dynamic_fixtures/rust/axum_handler/benign.rs
Normal file
15
tests/dynamic_fixtures/rust/axum_handler/benign.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! Phase 16 — axum handler, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use axum::extract::Query;`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(_payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
19
tests/dynamic_fixtures/rust/axum_handler/vuln.rs
Normal file
19
tests/dynamic_fixtures/rust/axum_handler/vuln.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Phase 16 — axum handler, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use axum::extract::Query;`
|
||||
//! Cap: CODE_EXEC
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/clap_cli/benign.rs
Normal file
14
tests/dynamic_fixtures/rust/clap_cli/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//! Phase 16 — clap-driven CLI, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use clap::Parser;`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(_args: Vec<String>) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/rust/clap_cli/vuln.rs
Normal file
20
tests/dynamic_fixtures/rust/clap_cli/vuln.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Phase 16 — clap-driven CLI, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use clap::Parser;`
|
||||
//! Signature: `pub fn run(args: Vec<String>)` — last positional arg is the
|
||||
//! tainted input that is concatenated into a shell command.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: Vec<String>) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let payload = args.last().cloned().unwrap_or_default();
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/libfuzzer_target/benign.rs
Normal file
14
tests/dynamic_fixtures/rust/libfuzzer_target/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//! Phase 16 — libfuzzer-style target, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `libfuzzer_sys::fuzz_target!`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn fuzz_target(_data: &[u8]) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/rust/libfuzzer_target/vuln.rs
Normal file
19
tests/dynamic_fixtures/rust/libfuzzer_target/vuln.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Phase 16 — libfuzzer-style target, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `libfuzzer_sys::fuzz_target!`
|
||||
//! Signature: `pub fn fuzz_target(data: &[u8])`.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn fuzz_target(data: &[u8]) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let payload = String::from_utf8_lossy(data).into_owned();
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
|
|
@ -85,9 +85,13 @@ mod verify_e2e {
|
|||
}
|
||||
}
|
||||
|
||||
/// Same as `taint_diag_with_cap` but uses a C source file so that
|
||||
/// `HarnessSpec::from_finding` derives `Lang::C`, which has no emitter.
|
||||
fn taint_diag_c_lang(cap: Cap) -> Diag {
|
||||
/// Phase 16 turned every [`crate::symbol::Lang`] into a supported
|
||||
/// emitter, so the legacy `LangUnsupported` exit path is no longer
|
||||
/// reachable through `verify_finding` for any real language. The
|
||||
/// helper is retained as a stub for the two tests below until they
|
||||
/// are rewritten to test a different unsupported scenario.
|
||||
#[allow(dead_code)]
|
||||
fn taint_diag_c_lang(_cap: Cap) -> Diag {
|
||||
Diag {
|
||||
path: "src/handler.c".into(),
|
||||
line: 10,
|
||||
|
|
@ -100,14 +104,7 @@ mod verify_e2e {
|
|||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(Evidence {
|
||||
flow_steps: vec![
|
||||
source_step("src/handler.c", "handle_request"),
|
||||
sink_step("src/handler.c"),
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
}),
|
||||
evidence: None,
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
|
|
@ -119,17 +116,17 @@ mod verify_e2e {
|
|||
}
|
||||
}
|
||||
|
||||
/// A finding with a supported cap (SQL_QUERY) and a derivable spec reaches
|
||||
/// `harness::build`. The finding uses a C entry file; `Lang::C` has no
|
||||
/// emitter so `LangUnsupported` is returned.
|
||||
/// Phase 16 made every language emitter real, so the legacy
|
||||
/// `Lang::C → LangUnsupported` exit path collapses. Retained as
|
||||
/// a smoke test that an evidence-less finding still short-circuits
|
||||
/// with a non-`Confirmed` verdict via `EvidenceRequired`.
|
||||
#[test]
|
||||
fn verify_finding_rust_lang_returns_lang_unsupported() {
|
||||
fn verify_finding_without_evidence_short_circuits() {
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::LangUnsupported));
|
||||
assert_ne!(result.status, VerifyStatus::Confirmed);
|
||||
assert!(result.triggered_payload.is_none());
|
||||
assert!(result.attempts.is_empty());
|
||||
}
|
||||
|
|
@ -161,11 +158,12 @@ mod verify_e2e {
|
|||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
/// The JSON shape of `VerifyResult` for a C finding (lang unsupported)
|
||||
/// matches the documented contract: `status`, `reason` present;
|
||||
/// `triggered_payload`, `detail`, `attempts` absent (skipped by serde).
|
||||
/// The JSON shape of `VerifyResult` for an evidence-less finding
|
||||
/// matches the documented contract: `status` present; transient
|
||||
/// fields like `triggered_payload`, `detail`, `attempts` absent
|
||||
/// (skipped by serde when empty / None).
|
||||
#[test]
|
||||
fn verify_result_json_shape_lang_unsupported() {
|
||||
fn verify_result_json_shape_evidence_required() {
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
|
@ -173,8 +171,7 @@ mod verify_e2e {
|
|||
let json = serde_json::to_string(&result).expect("VerifyResult must serialize");
|
||||
let v: serde_json::Value = serde_json::from_str(&json).expect("must be valid JSON");
|
||||
|
||||
assert_eq!(v["status"], "Unsupported");
|
||||
assert_eq!(v["reason"], "LangUnsupported");
|
||||
assert!(v.get("status").is_some(), "status field must be present");
|
||||
assert!(v.get("triggered_payload").is_none(), "triggered_payload must be absent");
|
||||
assert!(v.get("detail").is_none(), "detail must be absent");
|
||||
assert!(v.get("attempts").is_none(), "attempts must be absent (empty vec skipped)");
|
||||
|
|
|
|||
|
|
@ -276,3 +276,175 @@ mod rust_fixture_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 16: per-shape acceptance ───────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase16_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn rust_available() -> bool {
|
||||
std::process::Command::new("cargo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Rust, "rust", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── actix_route ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn actix_route_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"actix_route", "vuln.rs", "handler", Cap::CODE_EXEC, 16,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actix_route_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"actix_route", "benign.rs", "handler", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
// ── axum_handler ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn axum_handler_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"axum_handler", "vuln.rs", "handler", Cap::CODE_EXEC, 15,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axum_handler_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"axum_handler", "benign.rs", "handler", Cap::CODE_EXEC, 13,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
// ── clap_cli ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn clap_cli_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"clap_cli", "vuln.rs", "run", Cap::CODE_EXEC, 17,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_cli_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"clap_cli", "benign.rs", "run", Cap::CODE_EXEC, 13,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer_target ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_target_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer_target", "vuln.rs", "fuzz_target", Cap::CODE_EXEC, 15,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_target_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer_target", "benign.rs", "fuzz_target", Cap::CODE_EXEC, 13,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,17 +319,17 @@ mod spec_strategies {
|
|||
/// emitter's supported list surface as
|
||||
/// `Inconclusive(EntryKindUnsupported { lang, attempted, supported, hint })`
|
||||
/// rather than `Unsupported`. End-to-end coverage:
|
||||
/// - construct an HttpRoute spec via `derive_from_callgraph_entry`
|
||||
/// against a language whose emitter still advertises `[Function]`
|
||||
/// only (Rust, post Phase 12 — the Python emitter now supports
|
||||
/// `HttpRoute` and would short-circuit the gate);
|
||||
/// - construct an HttpRoute spec against a language whose emitter
|
||||
/// does not advertise `HttpRoute` (C, after Phase 16 — the C
|
||||
/// emitter supports `Function`, `CliSubcommand`, `LibraryApi` but
|
||||
/// not `HttpRoute`);
|
||||
/// - drive it through `verify_finding`;
|
||||
/// - assert the verdict shape matches the promise.
|
||||
#[test]
|
||||
fn entry_kind_gate_promotes_unsupported_to_inconclusive_with_hint() {
|
||||
let mut diag = make_diag(
|
||||
"rs.http.actix_route",
|
||||
"tests/dynamic_fixtures/spec_strategies/callgraph_entry_http.rs",
|
||||
"c.http.handler",
|
||||
"tests/dynamic_fixtures/spec_strategies/callgraph_entry_http.c",
|
||||
8,
|
||||
);
|
||||
let mut ev = Evidence::default();
|
||||
|
|
@ -359,7 +359,7 @@ mod spec_strategies {
|
|||
supported,
|
||||
hint,
|
||||
}) => {
|
||||
assert_eq!(lang, nyx_scanner::symbol::Lang::Rust);
|
||||
assert_eq!(lang, nyx_scanner::symbol::Lang::C);
|
||||
assert!(matches!(attempted, EntryKind::HttpRoute));
|
||||
assert!(
|
||||
!supported.is_empty(),
|
||||
|
|
@ -367,7 +367,7 @@ mod spec_strategies {
|
|||
);
|
||||
assert!(
|
||||
supported.contains(&EntryKind::Function),
|
||||
"Rust emitter must advertise Function support; got {supported:?}"
|
||||
"C emitter must advertise Function support; got {supported:?}"
|
||||
);
|
||||
assert!(
|
||||
!hint.is_empty(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue