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

@ -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!(