refactor(dynamic): add multi-method support to RouteShape, update framework bindings, and improve test coverage

This commit is contained in:
elipeter 2026-05-23 10:08:41 -05:00
parent 4bcdec3a1b
commit ca075a7141
55 changed files with 524 additions and 215 deletions

View file

@ -545,6 +545,9 @@ pub fn prepare_go(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, Bui
fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> {
let go_bin = std::env::var("NYX_GO_BIN").unwrap_or_else(|_| "go".to_owned());
let go_cache = std::env::var("GOCACHE")
.unwrap_or_else(|_| workdir.join(".gocache").to_string_lossy().into_owned());
std::fs::create_dir_all(&go_cache).map_err(|e| format!("create GOCACHE: {e}"))?;
let output = Command::new(&go_bin)
.args([
"build",
@ -556,6 +559,7 @@ fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String>
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.env("GOCACHE", go_cache)
.env(
"GOPATH",
std::env::var("GOPATH").unwrap_or_else(|_| {

View file

@ -140,7 +140,9 @@ mod tests {
let tree = parse_java(src);
let summary = FuncSummary {
name: "sign".into(),
callees: vec![crate::summary::CalleeSite::bare("MessageDigest.getInstance")],
callees: vec![crate::summary::CalleeSite::bare(
"MessageDigest.getInstance",
)],
..Default::default()
};
assert!(
@ -169,7 +171,8 @@ mod tests {
#[test]
fn skips_plain_method() {
let src: &[u8] = b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
let src: &[u8] =
b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "add".into(),

View file

@ -122,7 +122,8 @@ mod tests {
#[test]
fn fires_on_math_random_key() {
let src: &[u8] = b"function run(value) { return Math.random(); }\nmodule.exports = { run };\n";
let src: &[u8] =
b"function run(value) { return Math.random(); }\nmodule.exports = { run };\n";
let tree = parse_js(src);
let summary = FuncSummary {
name: "run".into(),

View file

@ -128,8 +128,7 @@ mod tests {
#[test]
fn fires_on_md5() {
let src: &[u8] =
b"<?php\nfunction sign($value) {\n return md5($value);\n}\n";
let src: &[u8] = b"<?php\nfunction sign($value) {\n return md5($value);\n}\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "sign".into(),
@ -180,8 +179,7 @@ mod tests {
#[test]
fn skips_when_sha256_hashing_present() {
let src: &[u8] =
b"<?php\nfunction sign($value) {\n return hash('sha256', $value);\n}\n";
let src: &[u8] = b"<?php\nfunction sign($value) {\n return hash('sha256', $value);\n}\n";
let tree = parse_php(src);
let summary = FuncSummary {
name: "sign".into(),

View file

@ -159,7 +159,8 @@ mod tests {
#[test]
fn fires_on_md5_compute() {
let src: &[u8] = b"use md5;\npub fn sign(value: &[u8]) -> md5::Digest {\n md5::compute(value)\n}\n";
let src: &[u8] =
b"use md5;\npub fn sign(value: &[u8]) -> md5::Digest {\n md5::compute(value)\n}\n";
let tree = parse_rust(src);
let summary = FuncSummary {
name: "sign".into(),

View file

@ -34,10 +34,7 @@ fn callee_is_outbound_http(name: &str) -> bool {
}
fn source_imports_go_http_client(file_bytes: &[u8]) -> bool {
const NEEDLES: &[&[u8]] = &[
b"\"net/http\"",
b"net/http\"",
];
const NEEDLES: &[&[u8]] = &[b"\"net/http\"", b"net/http\""];
NEEDLES
.iter()
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))

View file

@ -213,7 +213,8 @@ mod tests {
#[test]
fn skips_plain_method() {
let src: &[u8] = b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
let src: &[u8] =
b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
let tree = parse_java(src);
let summary = FuncSummary {
name: "add".into(),

View file

@ -19,15 +19,7 @@ fn callee_is_outbound_http(name: &str) -> bool {
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
matches!(
last,
"urlopen"
| "get"
| "post"
| "put"
| "patch"
| "delete"
| "request"
| "Request"
| "send"
"urlopen" | "get" | "post" | "put" | "patch" | "delete" | "request" | "Request" | "send"
) || matches!(
name,
"urllib.request.urlopen"

View file

@ -19,33 +19,31 @@ const ADAPTER_NAME: &str = "data-exfil-ruby";
fn callee_is_outbound_http(name: &str) -> bool {
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
matches!(
last,
"get_response" | "post_form" | "request" | "start"
) || matches!(
name,
"Net::HTTP.get"
| "Net::HTTP.get_response"
| "Net::HTTP.post_form"
| "Net::HTTP.start"
| "Net::HTTP::Get.new"
| "Net::HTTP::Post.new"
| "RestClient.get"
| "RestClient.post"
| "RestClient.put"
| "RestClient.delete"
| "RestClient::Request.execute"
| "HTTParty.get"
| "HTTParty.post"
| "HTTParty.put"
| "HTTParty.delete"
| "Faraday.get"
| "Faraday.post"
| "Faraday.new"
| "Faraday::Connection.get"
| "URI.open"
| "Kernel.open"
)
matches!(last, "get_response" | "post_form" | "request" | "start")
|| matches!(
name,
"Net::HTTP.get"
| "Net::HTTP.get_response"
| "Net::HTTP.post_form"
| "Net::HTTP.start"
| "Net::HTTP::Get.new"
| "Net::HTTP::Post.new"
| "RestClient.get"
| "RestClient.post"
| "RestClient.put"
| "RestClient.delete"
| "RestClient::Request.execute"
| "HTTParty.get"
| "HTTParty.post"
| "HTTParty.put"
| "HTTParty.delete"
| "Faraday.get"
| "Faraday.post"
| "Faraday.new"
| "Faraday::Connection.get"
| "URI.open"
| "Kernel.open"
)
}
fn source_imports_ruby_http_client(file_bytes: &[u8]) -> bool {

View file

@ -56,7 +56,7 @@ impl FrameworkAdapter for GoChiAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -56,7 +56,7 @@ impl FrameworkAdapter for GoEchoAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -57,7 +57,7 @@ impl FrameworkAdapter for GoFiberAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -59,7 +59,7 @@ impl FrameworkAdapter for GoGinAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -83,10 +83,7 @@ fn detect_micronaut(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -87,10 +87,7 @@ fn detect_quarkus(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -81,10 +81,7 @@ fn detect_servlet(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -128,10 +128,7 @@ fn detect_spring(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -97,7 +97,7 @@ fn detect_express(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -99,7 +99,7 @@ fn detect_fastify(
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -148,7 +148,7 @@ fn detect_koa(
return Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,
@ -162,10 +162,7 @@ fn detect_koa(
return Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: HttpMethod::GET,
path: "/".to_owned(),
}),
route: Some(RouteShape::single(HttpMethod::GET, "/")),
request_params,
response_writer: None,
middleware: vec![MiddlewareShape {

View file

@ -120,10 +120,7 @@ fn detect_nest(
Some(FrameworkBinding {
adapter: adapter_name.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method,
path: full_path,
}),
route: Some(RouteShape::single(method, full_path)),
request_params,
response_writer: None,
middleware,

View file

@ -58,7 +58,7 @@ impl FrameworkAdapter for PhpCodeIgniterAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -14,15 +14,15 @@
#[cfg(test)]
use crate::dynamic::framework::HttpMethod;
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
use crate::evidence::EntryKind;
use crate::summary::FuncSummary;
use crate::symbol::Lang;
use tree_sitter::Node;
use super::php_routes::{
bind_php_path_params, collect_php_middleware, find_laravel_static_route, find_php_function,
php_class_name, php_formal_names, source_imports_laravel,
bind_php_path_params, collect_php_middleware, find_laravel_static_route_shape,
find_php_function, php_class_name, php_formal_names, source_imports_laravel,
};
pub struct PhpLaravelAdapter;
@ -50,16 +50,16 @@ impl FrameworkAdapter for PhpLaravelAdapter {
let (func_node, class) = find_php_function(ast, file_bytes, &summary.name)?;
let controller = class.and_then(|c| php_class_name(c, file_bytes));
let (method, path) = find_laravel_static_route(ast, file_bytes, &summary.name, controller)?;
let route = find_laravel_static_route_shape(ast, file_bytes, &summary.name, controller)?;
let formals = php_formal_names(func_node, file_bytes);
let request_params = bind_php_path_params(&formals, &path);
let request_params = bind_php_path_params(&formals, &route.path);
let middleware = collect_php_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(route),
request_params,
response_writer: None,
middleware,
@ -139,6 +139,22 @@ mod tests {
assert_eq!(binding.route.unwrap().method, HttpMethod::DELETE);
}
#[test]
fn preserves_match_route_methods() {
let src: &[u8] = b"<?php\nuse Illuminate\\Support\\Facades\\Route;\nRoute::match(['POST', 'PATCH'], '/jobs/{id}', [JobController::class, 'run']);\nclass JobController {\n public function run($id) { return $id; }\n}\n";
let tree = parse(src);
let binding = PhpLaravelAdapter
.detect(&summary("run"), tree.root_node(), src)
.expect("binding");
let route = binding.route.unwrap();
assert_eq!(route.method, HttpMethod::POST);
assert_eq!(
route.reachable_methods(),
vec![HttpMethod::POST, HttpMethod::PATCH]
);
assert_eq!(route.path, "/jobs/{id}");
}
#[test]
fn populates_middleware_from_chained_call() {
let src: &[u8] = b"<?php\nuse Illuminate\\Support\\Facades\\Route;\nRoute::get('/users/{id}', 'UserController@show')->middleware('auth');\nclass UserController {\n public function show($id) { return $id; }\n}\n";

View file

@ -11,7 +11,7 @@
//! framework share the same placeholder-binding semantics.
use crate::dynamic::framework::{
HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers,
HttpMethod, MiddlewareShape, ParamBinding, ParamSource, RouteShape, auth_markers,
};
use crate::symbol::Lang;
use tree_sitter::Node;
@ -407,7 +407,19 @@ pub fn find_laravel_static_route<'a>(
target: &str,
controller: Option<&str>,
) -> Option<(HttpMethod, String)> {
let mut hit: Option<(HttpMethod, String)> = None;
find_laravel_static_route_shape(root, bytes, target, controller)
.map(|route| (route.method, route.path))
}
/// Laravel route lookup that preserves multi-verb registrations such
/// as `Route::any(...)` and `Route::match([...], ...)`.
pub fn find_laravel_static_route_shape<'a>(
root: Node<'a>,
bytes: &'a [u8],
target: &str,
controller: Option<&str>,
) -> Option<RouteShape> {
let mut hit: Option<RouteShape> = None;
visit_laravel_routes(root, bytes, target, controller, &mut hit);
hit
}
@ -417,7 +429,7 @@ fn visit_laravel_routes<'a>(
bytes: &'a [u8],
target: &str,
controller: Option<&str>,
out: &mut Option<(HttpMethod, String)>,
out: &mut Option<RouteShape>,
) {
if out.is_some() {
return;
@ -439,20 +451,81 @@ fn try_laravel_route<'a>(
bytes: &'a [u8],
target: &str,
controller: Option<&str>,
) -> Option<(HttpMethod, String)> {
) -> Option<RouteShape> {
let scope = call.child_by_field_name("scope")?.utf8_text(bytes).ok()?;
let scope_leaf = scope.rsplit('\\').next().unwrap_or(scope);
if scope_leaf != "Route" {
return None;
}
let verb_node = call.child_by_field_name("name")?.utf8_text(bytes).ok()?;
let method = verb_method(verb_node)?;
let args = call.child_by_field_name("arguments")?;
let path = first_php_string_arg(args, bytes)?;
if !laravel_callable_matches(args, bytes, target, controller) {
let methods = laravel_route_methods(verb_node, args, bytes)?;
let path = laravel_route_path(verb_node, args, bytes)?;
if !laravel_callable_matches(verb_node, args, bytes, target, controller) {
return None;
}
Some((method, path))
Some(if methods.len() > 1 {
RouteShape::multi(methods, path)
} else {
RouteShape::single(methods[0], path)
})
}
fn laravel_route_methods(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<Vec<HttpMethod>> {
match verb {
"any" => Some(vec![
HttpMethod::GET,
HttpMethod::HEAD,
HttpMethod::POST,
HttpMethod::PUT,
HttpMethod::PATCH,
HttpMethod::DELETE,
HttpMethod::OPTIONS,
]),
"match" => {
let first = positional_arg_values(arguments).into_iter().next()?;
let mut methods = Vec::new();
collect_http_methods(first, bytes, &mut methods);
if methods.is_empty() {
None
} else {
Some(methods)
}
}
other => verb_method(other).map(|method| vec![method]),
}
}
fn laravel_route_path(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<String> {
if verb != "match" {
return first_php_string_arg(arguments, bytes);
}
positional_arg_values(arguments)
.get(1)
.and_then(|value| string_content(*value, bytes))
}
fn positional_arg_values<'a>(arguments: Node<'a>) -> Vec<Node<'a>> {
let mut cur = arguments.walk();
arguments
.named_children(&mut cur)
.filter(|arg| arg.kind() == "argument" && arg.child_by_field_name("name").is_none())
.filter_map(|arg| arg.named_child(0))
.collect()
}
fn collect_http_methods(node: Node<'_>, bytes: &[u8], out: &mut Vec<HttpMethod>) {
if matches!(node.kind(), "string" | "encapsed_string")
&& let Some(raw) = string_content(node, bytes)
&& let Some(method) = HttpMethod::from_ident(&raw)
&& !out.contains(&method)
{
out.push(method);
}
let mut cur = node.walk();
for child in node.named_children(&mut cur) {
collect_http_methods(child, bytes, out);
}
}
/// Check the second positional arg of a `Route::verb('/x', ...)` call
@ -462,26 +535,15 @@ fn try_laravel_route<'a>(
/// - `'Controller@method'` / `'Controller::method'` strings
/// - `[ Controller::class, 'method' ]` arrays
fn laravel_callable_matches(
verb: &str,
arguments: Node<'_>,
bytes: &[u8],
target: &str,
controller: Option<&str>,
) -> bool {
let mut cur = arguments.walk();
let mut positional: Vec<Node<'_>> = Vec::new();
for arg in arguments.named_children(&mut cur) {
if arg.kind() != "argument" {
continue;
}
if arg.child_by_field_name("name").is_some() {
continue;
}
positional.push(arg);
}
let Some(callable_arg) = positional.get(1) else {
return false;
};
let Some(value) = callable_arg.named_child(0) else {
let callable_idx = if verb == "match" { 2 } else { 1 };
let positional = positional_arg_values(arguments);
let Some(value) = positional.get(callable_idx).copied() else {
return false;
};
match value.kind() {
@ -557,7 +619,6 @@ fn verb_method(verb: &str) -> Option<HttpMethod> {
"delete" => Some(HttpMethod::DELETE),
"options" => Some(HttpMethod::OPTIONS),
"head" => Some(HttpMethod::HEAD),
"any" | "match" => Some(HttpMethod::GET),
_ => None,
}
}
@ -895,6 +956,46 @@ mod tests {
assert_eq!(hit.1, "/users");
}
#[test]
fn finds_laravel_any_route_with_all_supported_methods() {
let src: &[u8] =
b"<?php\nRoute::any('/run', 'JobController@run');\nclass JobController { public function run() {} }\n";
let tree = parse(src);
let route =
find_laravel_static_route_shape(tree.root_node(), src, "run", Some("JobController"))
.unwrap();
assert_eq!(route.method, HttpMethod::GET);
assert_eq!(
route.reachable_methods(),
vec![
HttpMethod::GET,
HttpMethod::HEAD,
HttpMethod::POST,
HttpMethod::PUT,
HttpMethod::PATCH,
HttpMethod::DELETE,
HttpMethod::OPTIONS,
]
);
assert_eq!(route.path, "/run");
}
#[test]
fn finds_laravel_match_route_with_declared_methods() {
let src: &[u8] =
b"<?php\nRoute::match(['POST', 'PUT'], '/run', [JobController::class, 'run']);\nclass JobController { public function run() {} }\n";
let tree = parse(src);
let route =
find_laravel_static_route_shape(tree.root_node(), src, "run", Some("JobController"))
.unwrap();
assert_eq!(route.method, HttpMethod::POST);
assert_eq!(
route.reachable_methods(),
vec![HttpMethod::POST, HttpMethod::PUT]
);
assert_eq!(route.path, "/run");
}
#[test]
fn finds_codeigniter_member_route() {
let src: &[u8] = b"<?php\n$routes->get('users/(:num)', 'UserController::show');\n";

View file

@ -89,10 +89,7 @@ impl FrameworkAdapter for PhpSymfonyAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -213,7 +213,7 @@ impl FrameworkAdapter for PythonDjangoAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware: Vec::new(),

View file

@ -263,7 +263,7 @@ impl FrameworkAdapter for PythonFastApiAdapter {
return Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware: Vec::new(),

View file

@ -122,7 +122,7 @@ impl FrameworkAdapter for PythonFlaskAdapter {
return Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware: Vec::new(),

View file

@ -125,7 +125,7 @@ impl FrameworkAdapter for PythonStarletteAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware: Vec::new(),

View file

@ -180,10 +180,7 @@ impl FrameworkAdapter for RubyHanamiAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -289,10 +289,7 @@ impl FrameworkAdapter for RubyRailsAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: http_method,
path,
}),
route: Some(RouteShape::single(http_method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -153,10 +153,7 @@ impl FrameworkAdapter for RubySinatraAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: route.method,
path: route.path.clone(),
}),
route: Some(RouteShape::single(route.method, route.path.clone())),
request_params,
response_writer: None,
middleware,

View file

@ -54,7 +54,7 @@ impl FrameworkAdapter for RustActixAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -56,7 +56,7 @@ impl FrameworkAdapter for RustAxumAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -57,7 +57,7 @@ impl FrameworkAdapter for RustRocketAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -58,7 +58,7 @@ impl FrameworkAdapter for RustWarpAdapter {
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape { method, path }),
route: Some(RouteShape::single(method, path)),
request_params,
response_writer: None,
middleware,

View file

@ -37,12 +37,56 @@ pub use crate::entry_points::HttpMethod;
pub struct RouteShape {
/// HTTP verb (`GET`, `POST`, …).
pub method: HttpMethod,
/// Additional HTTP verbs that reach the same handler. Empty for
/// single-verb routes; when populated, [`Self::method`] is the
/// first element for backward-compatible callers that still need a
/// single representative method.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub methods: Vec<HttpMethod>,
/// Route path template as registered with the framework (e.g.
/// `"/users/{id}"`). Adapter-specific placeholder syntax is
/// preserved verbatim.
pub path: String,
}
impl RouteShape {
/// Construct a single-method route while preserving the legacy
/// empty-`methods` representation.
pub fn single(method: HttpMethod, path: impl Into<String>) -> Self {
Self {
method,
methods: Vec::new(),
path: path.into(),
}
}
/// Construct a route reachable through multiple HTTP methods.
pub fn multi(methods: Vec<HttpMethod>, path: impl Into<String>) -> Self {
let mut deduped = Vec::new();
for method in methods {
if !deduped.contains(&method) {
deduped.push(method);
}
}
let method = deduped.first().copied().unwrap_or(HttpMethod::GET);
Self {
method,
methods: deduped,
path: path.into(),
}
}
/// Return every method that reaches this route. Legacy single-method
/// shapes return a one-element vector containing [`Self::method`].
pub fn reachable_methods(&self) -> Vec<HttpMethod> {
if self.methods.is_empty() {
vec![self.method]
} else {
self.methods.clone()
}
}
}
/// Where on the external surface a function formal originates from.
///
/// Adapters classify each declared parameter into one of these
@ -532,10 +576,7 @@ mod tests {
let original = FrameworkBinding {
adapter: "flask".into(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: HttpMethod::POST,
path: "/users/{id}".into(),
}),
route: Some(RouteShape::single(HttpMethod::POST, "/users/{id}")),
request_params: vec![ParamBinding {
index: 0,
name: "id".into(),

View file

@ -1531,11 +1531,7 @@ func nyxJsonParseViaFixture(payload string) (int, bool, bool) {{
"##
);
let invoke = "\tdepth, excessive, fixtureInvoked := nyxJsonParseViaFixture(payload)\n\tif !fixtureInvoked {\n\t\tdepth = 0\n\t\texcessive = false\n\t}\n\tnyxJsonParseProbe(depth, excessive)\n".to_owned();
(
"\n\t\"nyx-harness/internal/vulnentry\"\n",
decl,
invoke,
)
("\n\t\"nyx-harness/internal/vulnentry\"\n", decl, invoke)
} else {
(
"",
@ -1775,10 +1771,10 @@ func nyxInstallHttpTransport() {
func nyxDataExfilViaFixture(payload string) {
defer func() { _ = recover() }()
vulnentry."##.to_owned()
vulnentry."##
.to_owned()
+ &format!("{entry_fn}(payload)\n}}\n\n");
let invoke =
"\tnyxInstallHttpTransport()\n\tnyxDataExfilViaFixture(payload)\n".to_owned();
let invoke = "\tnyxInstallHttpTransport()\n\tnyxDataExfilViaFixture(payload)\n".to_owned();
(
"\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"nyx-harness/internal/vulnentry\"\n",
decl,
@ -1855,9 +1851,15 @@ fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/signal"
"reflect"
"strings"
"syscall"
"time"
"nyx-harness/entry"
)
@ -1883,6 +1885,11 @@ func nyxPayload() string {{
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
return v
}}
if b64 := os.Getenv("NYX_PAYLOAD_B64"); b64 != "" {{
if data, err := base64.StdEncoding.DecodeString(b64); err == nil {{
return string(data)
}}
}}
return ""
}}
@ -2037,7 +2044,7 @@ fn emit_message_handler_harness(spec: &HarnessSpec, queue: &str) -> HarnessSourc
if got.Type().AssignableTo(want) {{
args[i] = got
}} else if want.Kind() == reflect.String {{
args[i] = reflect.ValueOf(os.Getenv("NYX_PAYLOAD"))
args[i] = reflect.ValueOf(nyxPayload())
}} else {{
args[i] = reflect.Zero(want)
}}
@ -2053,9 +2060,15 @@ fn emit_message_handler_harness(spec: &HarnessSpec, queue: &str) -> HarnessSourc
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/signal"
"reflect"
"strings"
"syscall"
"time"
"nyx-harness/entry"
)
@ -2070,6 +2083,11 @@ func nyxPayload() string {{
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
return v
}}
if b64 := os.Getenv("NYX_PAYLOAD_B64"); b64 != "" {{
if data, err := base64.StdEncoding.DecodeString(b64); err == nil {{
return string(data)
}}
}}
return ""
}}
@ -3170,7 +3188,8 @@ mod tests {
"Go UNAUTHORIZED_ID harness must pin caller_id to \"alice\"",
);
assert!(
h.source.contains("nyxIdorAccessProbe(_NYX_CALLER_ID, payload)"),
h.source
.contains("nyxIdorAccessProbe(_NYX_CALLER_ID, payload)"),
"Go UNAUTHORIZED_ID harness must call probe with caller_id + payload-as-owner",
);
}
@ -3182,7 +3201,8 @@ mod tests {
"Run",
));
assert!(
h.source.contains("if nyxUnauthorizedIdViaFixture(payload) {"),
h.source
.contains("if nyxUnauthorizedIdViaFixture(payload) {"),
"Go UNAUTHORIZED_ID harness must gate probe emission on a present record so the benign fixture's empty-string rejection clears the predicate",
);
assert!(
@ -3236,7 +3256,8 @@ mod tests {
"fallback path must not stage a vulnentry copy when the fixture cannot be read",
);
assert!(
h.source.contains("nyxIdorAccessProbe(_NYX_CALLER_ID, payload)"),
h.source
.contains("nyxIdorAccessProbe(_NYX_CALLER_ID, payload)"),
"fallback path must still emit an IDOR probe so the universal sink-hit path fires",
);
}

View file

@ -2360,7 +2360,10 @@ public class NyxHarness {{
".".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: vec![("NyxJsonProbe.java".to_owned(), nyx_json_probe_source().to_owned())],
extra_files: vec![(
"NyxJsonProbe.java".to_owned(),
nyx_json_probe_source().to_owned(),
)],
entry_subpath: Some(format!("{entry_class}.java")),
}
}
@ -3572,11 +3575,51 @@ public class NyxHarness {{
".".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: vec![],
extra_files: message_handler_annotation_stubs(),
entry_subpath: Some(format!("{entry_class}.java")),
}
}
fn message_handler_annotation_stubs() -> Vec<(String, String)> {
vec![
(
"org/springframework/kafka/annotation/KafkaListener.java".to_owned(),
r#"package org.springframework.kafka.annotation;
public @interface KafkaListener {
String[] value() default {};
String[] topics() default {};
}
"#
.to_owned(),
),
(
"io/awspring/cloud/sqs/annotation/SqsListener.java".to_owned(),
r#"package io.awspring.cloud.sqs.annotation;
public @interface SqsListener {
String[] value() default {};
String[] queueNames() default {};
String queueName() default "";
String queueUrl() default "";
}
"#
.to_owned(),
),
(
"org/springframework/amqp/rabbit/annotation/RabbitListener.java".to_owned(),
r#"package org.springframework.amqp.rabbit.annotation;
public @interface RabbitListener {
String[] value() default {};
String[] queues() default {};
}
"#
.to_owned(),
),
]
}
// ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ─────────────────────
fn emit_scheduled_job_harness(
@ -5363,4 +5406,26 @@ mod tests {
"Java DATA_EXFIL harness must catch InvocationTargetException so a fixture-side throw after a partial outbound call still drains CAPTURED_HOSTS",
);
}
#[test]
fn emit_message_handler_harness_ships_broker_annotation_stubs() {
let mut spec = make_spec(PayloadSlot::Param(0));
spec.entry_file = "tests/dynamic_fixtures/message_handler/kafka_java/Vuln.java".to_owned();
spec.entry_name = "onMessage".to_owned();
spec.entry_kind = EntryKind::MessageHandler {
queue: "orders".to_owned(),
message_schema: None,
};
let h = emit(&spec).unwrap();
for path in [
"org/springframework/kafka/annotation/KafkaListener.java",
"io/awspring/cloud/sqs/annotation/SqsListener.java",
"org/springframework/amqp/rabbit/annotation/RabbitListener.java",
] {
assert!(
h.extra_files.iter().any(|(name, _)| name == path),
"Java MessageHandler harness must stage {path} so annotated broker fixtures compile without real Spring jars",
);
}
}
}

View file

@ -3253,10 +3253,7 @@ mod tests {
spec.framework = Some(FrameworkBinding {
adapter: "test-adapter".into(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: HttpMethod::GET,
path: route_path.into(),
}),
route: Some(RouteShape::single(HttpMethod::GET, route_path)),
request_params: vec![],
response_writer: None,
middleware: vec![],

View file

@ -3986,10 +3986,8 @@ mod tests {
#[test]
fn emit_unauthorized_id_harness_derives_basename_from_entry_file() {
let h = emit_unauthorized_id_harness(&make_unauthorized_id_spec(
"/abs/path/benign.php",
"run",
));
let h =
emit_unauthorized_id_harness(&make_unauthorized_id_spec("/abs/path/benign.php", "run"));
assert!(
h.source.contains("\"benign.php\""),
"PHP UNAUTHORIZED_ID harness must use the entry-file basename, not a hard-coded literal: {}",

View file

@ -4706,8 +4706,7 @@ mod tests {
"run",
));
assert!(
h.source
.contains("def _nyx_idor_via_fixture(payload):"),
h.source.contains("def _nyx_idor_via_fixture(payload):"),
"Python UNAUTHORIZED_ID harness must define the fixture-routing helper",
);
assert!(h.source.contains("importlib.import_module(\"vuln\")"));
@ -4718,10 +4717,8 @@ mod tests {
#[test]
fn emit_unauthorized_id_harness_derives_module_name_from_entry_file() {
let h = emit_unauthorized_id_harness(&make_unauthorized_id_spec(
"/abs/path/benign.py",
"run",
));
let h =
emit_unauthorized_id_harness(&make_unauthorized_id_spec("/abs/path/benign.py", "run"));
assert!(h.source.contains("importlib.import_module(\"benign\")"));
}
@ -4758,7 +4755,10 @@ mod tests {
"run",
));
assert!(h.source.contains("urllib.request.urlopen = _nyx_urlopen"));
assert!(h.source.contains("def _nyx_urlopen(url, data=None, timeout=None, *args, **kwargs):"));
assert!(
h.source
.contains("def _nyx_urlopen(url, data=None, timeout=None, *args, **kwargs):")
);
assert!(
h.source.contains("class _NyxFakeResponse(io.BytesIO):"),
"harness must return a fake response so the fixture does not block on real network egress",
@ -4806,10 +4806,7 @@ mod tests {
#[test]
fn emit_data_exfil_harness_derives_module_name_from_entry_file() {
let h = emit_data_exfil_harness(&make_data_exfil_spec(
"/abs/path/benign.py",
"run",
));
let h = emit_data_exfil_harness(&make_data_exfil_spec("/abs/path/benign.py", "run"));
assert!(h.source.contains("importlib.import_module(\"benign\")"));
}
}

View file

@ -2864,7 +2864,10 @@ mod tests {
"tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb",
"run",
));
assert!(h.source.contains("_nyx_orig_json_parse = JSON.method(:parse)"));
assert!(
h.source
.contains("_nyx_orig_json_parse = JSON.method(:parse)")
);
assert!(
h.source.contains("JSON.define_singleton_method(:parse)"),
"must rebind JSON.parse: {}",
@ -2956,8 +2959,7 @@ mod tests {
h.source
);
assert!(
h.source
.contains("_nyx_idor_probe(NYX_CALLER_ID, payload)"),
h.source.contains("_nyx_idor_probe(NYX_CALLER_ID, payload)"),
"harness must emit the IDOR probe with the hard-coded caller and the payload owner_id: {}",
h.source
);
@ -2983,8 +2985,7 @@ mod tests {
"run",
));
assert!(
h.source
.contains("def _nyx_idor_via_fixture(payload)"),
h.source.contains("def _nyx_idor_via_fixture(payload)"),
"Ruby UNAUTHORIZED_ID harness must define the fixture-routing helper: {}",
h.source
);
@ -2996,10 +2997,8 @@ mod tests {
#[test]
fn emit_unauthorized_id_harness_derives_entry_basename_from_entry_file() {
let h = emit_unauthorized_id_harness(&make_unauthorized_id_spec(
"/abs/path/benign.rb",
"run",
));
let h =
emit_unauthorized_id_harness(&make_unauthorized_id_spec("/abs/path/benign.rb", "run"));
assert!(h.source.contains("require_relative './benign'"));
}
@ -3019,8 +3018,7 @@ mod tests {
))
.unwrap();
assert!(
h.source
.contains("Net::HTTP.define_singleton_method(:get)"),
h.source.contains("Net::HTTP.define_singleton_method(:get)"),
"dispatcher must short-circuit Cap::DATA_EXFIL into emit_data_exfil_harness: {}",
h.source
);

View file

@ -1593,8 +1593,7 @@ pub fn emit_unauthorized_id_harness(spec: &HarnessSpec) -> HarnessSource {
)
} else {
let extras: Vec<(String, String)> = vec![("Cargo.toml".into(), cargo_toml)];
let invoke =
" nyx_idor_access_probe(_NYX_CALLER_ID, &payload);\n".to_owned();
let invoke = " nyx_idor_access_probe(_NYX_CALLER_ID, &payload);\n".to_owned();
(
String::new(),
invoke,
@ -1700,11 +1699,12 @@ pub fn emit_data_exfil_harness(spec: &HarnessSpec) -> HarnessSource {
let extras: Vec<(String, String)> = vec![
("Cargo.toml".into(), cargo_toml),
("src/entry.rs".into(), rewritten),
("src/nyx_http.rs".into(), nyx_http_module_source().to_owned()),
(
"src/nyx_http.rs".into(),
nyx_http_module_source().to_owned(),
),
];
let invoke = format!(
" let _ = entry::{entry_fn}(&payload);\n",
);
let invoke = format!(" let _ = entry::{entry_fn}(&payload);\n",);
(
"mod entry;\nmod nyx_http;\n".to_owned(),
invoke,
@ -3702,8 +3702,7 @@ mod tests {
"run",
));
assert!(
h.source
.contains("const _NYX_CALLER_ID: &str = \"alice\";"),
h.source.contains("const _NYX_CALLER_ID: &str = \"alice\";"),
"Rust UNAUTHORIZED_ID harness must pin caller_id to \"alice\"",
);
assert!(

View file

@ -134,10 +134,7 @@ mod tests {
FrameworkBinding {
adapter: "test-adapter".to_string(),
kind: EntryKind::HttpRoute,
route: Some(RouteShape {
method: HttpMethod::GET,
path: "/x".to_string(),
}),
route: Some(RouteShape::single(HttpMethod::GET, "/x")),
request_params: Vec::new(),
response_writer: None,
middleware: middleware

View file

@ -288,7 +288,18 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts });
}
Err(_) => {}
Err(build_sandbox::BuildError::Io(e)) => {
return Err(RunError::BuildFailed {
stderr: format!("prepare go build cache: {e}"),
attempts: 1,
});
}
Err(build_sandbox::BuildError::Unsupported) => {
return Err(RunError::BuildFailed {
stderr: "go build preparation unsupported on this host".to_owned(),
attempts: 1,
});
}
}
}
Lang::Java => {
@ -306,7 +317,18 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts });
}
Err(_) => {}
Err(build_sandbox::BuildError::Io(e)) => {
return Err(RunError::BuildFailed {
stderr: format!("prepare java build cache: {e}"),
attempts: 1,
});
}
Err(build_sandbox::BuildError::Unsupported) => {
return Err(RunError::BuildFailed {
stderr: "java build preparation unsupported on this host".to_owned(),
attempts: 1,
});
}
}
}
Lang::Php => {

View file

@ -1616,6 +1616,10 @@ fn run_process(
// Strip all env and pass only the allowlist + harness env + payload.
cmd.env_clear();
// Keep a minimal executable search path so harnessed code that launches
// common system tools by bare name (notably Go's exec.Command("sh", ...))
// exercises the sink instead of failing before the oracle can observe it.
cmd.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
for k in &opts.env_passthrough {
if let Ok(v) = std::env::var(k) {
cmd.env(k, v);

View file

@ -1,8 +1,11 @@
// Phase 20 (Track M.2) Kafka Java benign control.
// `org.springframework.kafka` adapter marker preserved.
import org.springframework.kafka.annotation.KafkaListener;
public class Benign {
public Benign() {}
@KafkaListener(topics = "orders")
public void onMessage(String body) throws Exception {
new ProcessBuilder("echo", body).inheritIO().start().waitFor();
}

View file

@ -1,13 +1,11 @@
// Phase 20 (Track M.2) Kafka Java vuln fixture.
//
// Marker line so the kafka-java framework adapter binds:
// `org.springframework.kafka` consumer entry point. Annotation is
// elided so javac compiles without the Spring jar; the dynamic harness
// invokes onMessage reflectively.
import org.springframework.kafka.annotation.KafkaListener;
public class Vuln {
public Vuln() {}
@KafkaListener(topics = "orders")
public void onMessage(String body) throws Exception {
// SINK: tainted body concatenated into shell command
new ProcessBuilder("sh", "-c", "echo " + body).inheritIO().start().waitFor();

View file

@ -1,9 +1,11 @@
// Phase 20 (Track M.2) RabbitMQ Java benign control.
// `org.springframework.amqp.rabbit` adapter marker preserved.
import org.springframework.amqp.rabbit.annotation.RabbitListener;
public class Benign {
public Benign() {}
@RabbitListener(queues = "work")
public void onMessage(String messageId, String body) throws Exception {
new ProcessBuilder("echo", body).inheritIO().start().waitFor();
}

View file

@ -1,10 +1,11 @@
// Phase 20 (Track M.2) RabbitMQ Java vuln fixture.
// `org.springframework.amqp.rabbit` consumer marker preserved;
// annotation elided so javac compiles without the Spring AMQP jar.
import org.springframework.amqp.rabbit.annotation.RabbitListener;
public class Vuln {
public Vuln() {}
@RabbitListener(queues = "work")
public void onMessage(String messageId, String body) throws Exception {
// SINK: tainted body concatenated into shell command
new ProcessBuilder("sh", "-c", "echo " + body).inheritIO().start().waitFor();

View file

@ -1,9 +1,11 @@
// Phase 20 (Track M.2) SQS Java benign control.
// `io.awspring.cloud.sqs` adapter marker preserved.
import io.awspring.cloud.sqs.annotation.SqsListener;
public class Benign {
public Benign() {}
@SqsListener("jobs")
public void handleMessage(java.util.Map<String, String> env) throws Exception {
String body = env != null ? env.getOrDefault("Body", "") : "";
new ProcessBuilder("echo", body).inheritIO().start().waitFor();

View file

@ -1,10 +1,11 @@
// Phase 20 (Track M.2) SQS Java vuln fixture.
// `io.awspring.cloud.sqs` consumer entry point annotation elided so
// javac compiles without the Spring Cloud AWS jar.
import io.awspring.cloud.sqs.annotation.SqsListener;
public class Vuln {
public Vuln() {}
@SqsListener("jobs")
public void handleMessage(java.util.Map<String, String> env) throws Exception {
String body = env != null ? env.getOrDefault("Body", "") : "";
// SINK: tainted Body concatenated into shell command

View file

@ -183,7 +183,9 @@ mod e2e_json_parse_depth {
Lang::Go => "go",
Lang::Rust => "rust",
Lang::Java => "java",
_ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"),
_ => unreachable!(
"JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"
),
})
.join(fixture);
let tmp = TempDir::new().expect("create tempdir");
@ -230,7 +232,9 @@ mod e2e_json_parse_depth {
Lang::Go => "go",
Lang::Rust => "cargo",
Lang::Java => "javac",
_ => unreachable!("JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"),
_ => unreachable!(
"JSON_PARSE depth e2e covers JS / Python / Ruby / PHP / Go / Rust / Java only"
),
};
if !command_available(required) {
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {required}");

View file

@ -326,8 +326,9 @@ mod e2e_phase_20 {
use tempfile::TempDir;
fn command_available(bin: &str) -> bool {
let version_arg = if bin == "go" { "version" } else { "--version" };
Command::new(bin)
.arg("--version")
.arg(version_arg)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
@ -484,7 +485,10 @@ mod e2e_phase_20 {
let Some(outcome) = run(Lang::Python, "sqs_python", "vuln.py", "handler", "jobs") else {
return;
};
assert!(outcome.triggered_by.is_some());
assert!(
outcome.triggered_by.is_some(),
"sqs-python MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
@ -500,7 +504,10 @@ mod e2e_phase_20 {
) else {
return;
};
assert!(outcome.triggered_by.is_some());
assert!(
outcome.triggered_by.is_some(),
"pubsub-python MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
@ -516,7 +523,10 @@ mod e2e_phase_20 {
) else {
return;
};
assert!(outcome.triggered_by.is_some());
assert!(
outcome.triggered_by.is_some(),
"rabbit-python MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
@ -534,4 +544,71 @@ mod e2e_phase_20 {
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn kafka_java_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Java, "kafka_java", "Vuln.java", "onMessage", "orders")
else {
return;
};
assert!(
outcome.triggered_by.is_some(),
"kafka-java MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn sqs_java_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Java, "sqs_java", "Vuln.java", "handleMessage", "jobs")
else {
return;
};
assert!(
outcome.triggered_by.is_some(),
"sqs-java MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn rabbit_java_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Java, "rabbit_java", "Vuln.java", "onMessage", "work") else {
return;
};
assert!(
outcome.triggered_by.is_some(),
"rabbit-java MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn pubsub_go_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Go, "pubsub_go", "vuln.go", "OnMessage", "my-sub") else {
return;
};
assert!(
outcome.triggered_by.is_some(),
"pubsub-go MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn nats_go_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Go, "nats_go", "vuln.go", "OnMessage", "events") else {
return;
};
assert!(
outcome.triggered_by.is_some(),
"nats-go MessageHandler vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome.differential.as_ref().expect("Confirmed");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
}