mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
refactor(dynamic): standardize shell commands across fixtures, add __NYX_SINK_HIT__ markers, improve PHP support
This commit is contained in:
parent
ca075a7141
commit
fe09986a25
32 changed files with 707 additions and 71 deletions
|
|
@ -503,6 +503,7 @@ int main(int argc, char *argv[]) {{
|
|||
if (!payload) payload = (char*)"";
|
||||
__nyx_install_crash_guard("{symbol}");
|
||||
{symbol}(payload, strlen(payload));
|
||||
puts("__NYX_SINK_HIT__");
|
||||
return 0;
|
||||
}}
|
||||
|
||||
|
|
|
|||
|
|
@ -451,6 +451,7 @@ int main(int argc, char *argv[]) {{
|
|||
__nyx_install_crash_guard("{class}::{method}");
|
||||
{class} instance;
|
||||
instance.{method}(payload);
|
||||
std::cout << "__NYX_SINK_HIT__" << std::endl;
|
||||
return 0;
|
||||
}}
|
||||
|
||||
|
|
|
|||
|
|
@ -1928,6 +1928,7 @@ func main() {{
|
|||
}}
|
||||
}}
|
||||
out := m.Call(args)
|
||||
fmt.Println("__NYX_SINK_HIT__")
|
||||
if len(out) > 0 {{
|
||||
fmt.Println(out[0].Interface())
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3383,6 +3383,7 @@ public class NyxHarness {{
|
|||
mArgs[i] = params[i].equals(String.class) ? payload : nyxStubForType(params[i]);
|
||||
}}
|
||||
match.invoke(instance, mArgs);
|
||||
System.out.println("__NYX_SINK_HIT__");
|
||||
}} catch (InvocationTargetException ite) {{
|
||||
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
|
||||
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());
|
||||
|
|
|
|||
|
|
@ -725,14 +725,10 @@ fn emit_class_method(
|
|||
_spec: &HarnessSpec,
|
||||
class: &str,
|
||||
method: &str,
|
||||
is_typescript: bool,
|
||||
_is_typescript: bool,
|
||||
) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let entry_subpath = if is_typescript {
|
||||
"entry.ts"
|
||||
} else {
|
||||
"entry.js"
|
||||
};
|
||||
let entry_subpath = "entry.js";
|
||||
let entry_require_path = entry_require_path(entry_subpath);
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
|
|
@ -806,6 +802,7 @@ if (typeof _m !== 'function') {{
|
|||
(async () => {{
|
||||
try {{
|
||||
const _result = await Promise.resolve(_m.call(_instance, payload));
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
if (_result != null) process.stdout.write(String(_result) + '\n');
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
//! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
|
||||
use crate::dynamic::framework::HttpMethod;
|
||||
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
|
@ -1943,6 +1944,7 @@ fn generate_source(spec: &HarnessSpec, shape: PhpShape) -> String {
|
|||
let call_expr = build_call_expr(spec, shape, entry_fn);
|
||||
let shim = probe_shim();
|
||||
let toolchain_marker = build_toolchain_marker(shape);
|
||||
let route_methods_fn = build_route_methods_fn(spec);
|
||||
let crash_callee = if entry_fn.is_empty() {
|
||||
"main"
|
||||
} else {
|
||||
|
|
@ -1966,6 +1968,8 @@ function nyx_payload(): string {{
|
|||
return '';
|
||||
}}
|
||||
|
||||
{route_methods_fn}
|
||||
|
||||
$payload = nyx_payload();
|
||||
|
||||
// Phase 08 sink-site signal handler: install AFTER payload decode so a crash
|
||||
|
|
@ -1980,13 +1984,21 @@ __nyx_install_crash_guard('{crash_callee}');
|
|||
{entry_block}
|
||||
// ── Framework toolchain marker (Phase 16 — Track L.14) ────────────────────────
|
||||
{toolchain_marker}// ── Call entry point ──────────────────────────────────────────────────────────
|
||||
try {{
|
||||
$result = {call_expr};
|
||||
if ($result !== null) {{
|
||||
echo $result . "\n";
|
||||
foreach (__nyx_route_methods() as $__nyx_method) {{
|
||||
if ($__nyx_method !== '') {{
|
||||
$_SERVER['REQUEST_METHOD'] = $__nyx_method;
|
||||
putenv('REQUEST_METHOD=' . $__nyx_method);
|
||||
$_ENV['REQUEST_METHOD'] = $__nyx_method;
|
||||
$GLOBALS['__nyx_request_method'] = $__nyx_method;
|
||||
}}
|
||||
try {{
|
||||
$result = {call_expr};
|
||||
if ($result !== null) {{
|
||||
echo $result . "\n";
|
||||
}}
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||
}}
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||
}}
|
||||
"#,
|
||||
shape = shape,
|
||||
|
|
@ -1995,10 +2007,45 @@ try {{
|
|||
call_expr = call_expr,
|
||||
shim = shim,
|
||||
toolchain_marker = toolchain_marker,
|
||||
route_methods_fn = route_methods_fn,
|
||||
crash_callee = crash_callee,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_route_methods_fn(spec: &HarnessSpec) -> String {
|
||||
let mut methods = spec
|
||||
.framework
|
||||
.as_ref()
|
||||
.and_then(|binding| binding.route.as_ref())
|
||||
.map(|route| route.reachable_methods())
|
||||
.unwrap_or_default();
|
||||
if methods.is_empty() && matches!(spec.entry_kind, crate::evidence::EntryKind::HttpRoute) {
|
||||
methods.push(HttpMethod::GET);
|
||||
}
|
||||
let body = if methods.is_empty() {
|
||||
"''".to_owned()
|
||||
} else {
|
||||
methods
|
||||
.iter()
|
||||
.map(|method| format!("'{}'", http_method_name(*method)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
format!("function __nyx_route_methods(): array {{\n return [{body}];\n}}\n",)
|
||||
}
|
||||
|
||||
fn http_method_name(method: HttpMethod) -> &'static str {
|
||||
match method {
|
||||
HttpMethod::GET => "GET",
|
||||
HttpMethod::HEAD => "HEAD",
|
||||
HttpMethod::POST => "POST",
|
||||
HttpMethod::PUT => "PUT",
|
||||
HttpMethod::PATCH => "PATCH",
|
||||
HttpMethod::DELETE => "DELETE",
|
||||
HttpMethod::OPTIONS => "OPTIONS",
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pre_call(spec: &HarnessSpec, shape: PhpShape) -> String {
|
||||
let mut out = String::new();
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -2671,6 +2718,7 @@ if (!method_exists($instance, {method_lit:?})) {{
|
|||
}}
|
||||
try {{
|
||||
$result = call_user_func([$instance, {method_lit:?}], $payload);
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
if ($result !== null) {{
|
||||
echo $result . "\n";
|
||||
}}
|
||||
|
|
@ -2889,6 +2937,7 @@ fn function_exists_call(_func: &str) -> bool {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{FrameworkBinding, RouteShape};
|
||||
use crate::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
|
@ -3048,6 +3097,26 @@ mod tests {
|
|||
assert!(src.contains("$GLOBALS['__nyx_route']"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_shape_fans_out_framework_route_methods() {
|
||||
let mut spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
|
||||
spec.framework = Some(FrameworkBinding {
|
||||
adapter: "php-laravel".to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape::multi(
|
||||
vec![HttpMethod::GET, HttpMethod::POST, HttpMethod::PATCH],
|
||||
"/run",
|
||||
)),
|
||||
request_params: vec![],
|
||||
response_writer: None,
|
||||
middleware: vec![],
|
||||
});
|
||||
let src = generate_source(&spec, PhpShape::LaravelRoute);
|
||||
assert!(src.contains("return ['GET', 'POST', 'PATCH'];"));
|
||||
assert!(src.contains("foreach (__nyx_route_methods() as $__nyx_method)"));
|
||||
assert!(src.contains("$_SERVER['REQUEST_METHOD'] = $__nyx_method;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symfony_shape_emits_toolchain_marker_and_controller_dispatch() {
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
|
||||
|
|
|
|||
|
|
@ -875,6 +875,7 @@ try:
|
|||
print("NYX_METHOD_NOT_FOUND: " + {method:?}, file=sys.stderr, flush=True)
|
||||
sys.exit(78)
|
||||
_result = _m(payload)
|
||||
print("__NYX_SINK_HIT__", flush=True)
|
||||
if _result is not None:
|
||||
try:
|
||||
print(str(_result), flush=True)
|
||||
|
|
|
|||
|
|
@ -607,6 +607,7 @@ unless instance.respond_to?({method:?})
|
|||
end
|
||||
begin
|
||||
result = instance.send({method:?}, $nyx_payload)
|
||||
puts "__NYX_SINK_HIT__"
|
||||
print(result.to_s) if result
|
||||
rescue StandardError => e
|
||||
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
|
||||
|
|
|
|||
|
|
@ -2043,6 +2043,7 @@ fn main() {{
|
|||
__nyx_install_crash_guard("{entry_label}");
|
||||
let instance = entry::{class}::{ctor}();
|
||||
let _ = instance.{method}(&payload);
|
||||
println!("__NYX_SINK_HIT__");
|
||||
}}
|
||||
|
||||
fn nyx_payload() -> String {{
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@
|
|||
//! `ClassMethod`, drives it through `lang::emit`, and checks the
|
||||
//! harness source carries the matching `class` + `method` literal
|
||||
//! plus the per-lang structural marker (probe shim, build command,
|
||||
//! mock-class declaration when applicable).
|
||||
//! mock-class declaration when applicable). The `e2e_phase_19`
|
||||
//! submodule then drives the fixture pair through `run_spec` to pin
|
||||
//! the actual sandbox + oracle polarity.
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test class_method_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
mod common;
|
||||
|
||||
use nyx_scanner::dynamic::lang;
|
||||
use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
|
||||
use nyx_scanner::dynamic::stubs::{MockKind, mock_source};
|
||||
|
|
@ -51,7 +55,7 @@ fn entry_file(lang: Lang) -> &'static str {
|
|||
fn class_for(lang: Lang) -> (&'static str, &'static str) {
|
||||
match lang {
|
||||
Lang::Python => ("UserRepository", "find_by_name"),
|
||||
Lang::Java => ("UserRepository", "findByName"),
|
||||
Lang::Java => ("Vuln$UserRepository", "findByName"),
|
||||
Lang::C => ("UserService", "run"),
|
||||
_ => ("UserService", "run"),
|
||||
}
|
||||
|
|
@ -206,3 +210,303 @@ fn class_method_cpp_constructs_default_then_calls_method() {
|
|||
assert!(h.source.contains("UserService instance;"));
|
||||
assert!(h.source.contains("instance.run"));
|
||||
}
|
||||
|
||||
// ── End-to-end Phase 19 acceptance via run_spec ─────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod e2e_phase_19 {
|
||||
use super::*;
|
||||
use crate::common::fixture_harness::FIXTURE_LOCK;
|
||||
use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec};
|
||||
use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions};
|
||||
use nyx_scanner::dynamic::spec::{SpecDerivationStrategy, default_toolchain_id};
|
||||
use nyx_scanner::evidence::DifferentialVerdict;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Case {
|
||||
lang: Lang,
|
||||
fixture_dir: &'static str,
|
||||
vuln_file: &'static str,
|
||||
benign_file: &'static str,
|
||||
vuln_class: &'static str,
|
||||
benign_class: &'static str,
|
||||
method: &'static str,
|
||||
cap: Cap,
|
||||
bins: &'static [&'static str],
|
||||
}
|
||||
|
||||
const CASES: &[Case] = &[
|
||||
Case {
|
||||
lang: Lang::Python,
|
||||
fixture_dir: "python",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
vuln_class: "UserRepository",
|
||||
benign_class: "UserRepository",
|
||||
method: "find_by_name",
|
||||
cap: Cap::SQL_QUERY,
|
||||
bins: &["python3"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Ruby,
|
||||
fixture_dir: "ruby",
|
||||
vuln_file: "vuln.rb",
|
||||
benign_file: "benign.rb",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["ruby"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::JavaScript,
|
||||
fixture_dir: "javascript",
|
||||
vuln_file: "vuln.js",
|
||||
benign_file: "benign.js",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::TypeScript,
|
||||
fixture_dir: "typescript",
|
||||
vuln_file: "vuln.ts",
|
||||
benign_file: "benign.ts",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Php,
|
||||
fixture_dir: "php",
|
||||
vuln_file: "vuln.php",
|
||||
benign_file: "benign.php",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["php"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Java,
|
||||
fixture_dir: "java",
|
||||
vuln_file: "Vuln.java",
|
||||
benign_file: "Benign.java",
|
||||
vuln_class: "Vuln$UserRepository",
|
||||
benign_class: "Benign$UserRepository",
|
||||
method: "findByName",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["java", "javac"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Go,
|
||||
fixture_dir: "go",
|
||||
vuln_file: "vuln.go",
|
||||
benign_file: "benign.go",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "Run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["go"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Rust,
|
||||
fixture_dir: "rust",
|
||||
vuln_file: "vuln.rs",
|
||||
benign_file: "benign.rs",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["cargo"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::C,
|
||||
fixture_dir: "c",
|
||||
vuln_file: "vuln.c",
|
||||
benign_file: "benign.c",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["cc"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Cpp,
|
||||
fixture_dir: "cpp",
|
||||
vuln_file: "vuln.cpp",
|
||||
benign_file: "benign.cpp",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["c++"],
|
||||
},
|
||||
];
|
||||
|
||||
fn command_available(bin: &str) -> bool {
|
||||
Command::new(bin).arg("--version").output().is_ok()
|
||||
}
|
||||
|
||||
fn fixture_root(case: Case) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/class_method")
|
||||
.join(case.fixture_dir)
|
||||
}
|
||||
|
||||
fn build_spec(case: Case, file: &str, class: &str) -> (HarnessSpec, TempDir) {
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let src = fixture_root(case).join(file);
|
||||
let dst = tmp.path().join(file);
|
||||
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
|
||||
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"class-method|");
|
||||
digest.update(format!("{:?}", case.lang).as_bytes());
|
||||
digest.update(b"|");
|
||||
digest.update(case.fixture_dir.as_bytes());
|
||||
digest.update(b"|");
|
||||
digest.update(file.as_bytes());
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: case.method.to_owned(),
|
||||
entry_kind: EntryKind::ClassMethod {
|
||||
class: class.to_owned(),
|
||||
method: case.method.to_owned(),
|
||||
},
|
||||
lang: case.lang,
|
||||
toolchain_id: default_toolchain_id(case.lang).to_owned(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: case.cap,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 1,
|
||||
spec_hash,
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
|
||||
};
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
fn run(case: Case, file: &str, class: &str) -> Option<RunOutcome> {
|
||||
for bin in case.bins {
|
||||
if !command_available(bin) {
|
||||
eprintln!("SKIP {:?} {file}: missing toolchain {bin}", case.lang);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (spec, tmp) = build_spec(case, file, class);
|
||||
let repro = tmp.path().join("repro");
|
||||
let telemetry = tmp.path().join("events.jsonl");
|
||||
let build_cache = tmp.path().join("build-cache");
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", repro.to_str().unwrap());
|
||||
std::env::set_var("NYX_TELEMETRY_PATH", telemetry.to_str().unwrap());
|
||||
std::env::set_var("NYX_BUILD_CACHE", build_cache.to_str().unwrap());
|
||||
}
|
||||
let opts = SandboxOptions {
|
||||
backend: SandboxBackend::Process,
|
||||
..SandboxOptions::default()
|
||||
};
|
||||
let outcome = run_spec(&spec, &opts);
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
std::env::remove_var("NYX_BUILD_CACHE");
|
||||
}
|
||||
|
||||
match outcome {
|
||||
Ok(outcome) => Some(outcome),
|
||||
Err(RunError::BuildFailed { stderr, attempts }) => {
|
||||
eprintln!(
|
||||
"SKIP {:?} {file}: harness build failed after {attempts} attempts: {stderr}",
|
||||
case.lang,
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => panic!("run_spec({:?} {file}) errored: {e:?}", case.lang),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_confirmed(case: Case, outcome: &RunOutcome) {
|
||||
assert!(
|
||||
outcome.triggered_by.is_some(),
|
||||
"{:?} ClassMethod vuln must Confirm via run_spec; got {outcome:?}",
|
||||
case.lang,
|
||||
);
|
||||
let diff = outcome
|
||||
.differential
|
||||
.as_ref()
|
||||
.expect("Confirmed run must carry a DifferentialOutcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(case: Case, outcome: &RunOutcome) {
|
||||
assert!(
|
||||
outcome.triggered_by.is_none(),
|
||||
"{:?} ClassMethod benign control must not Confirm via run_spec; got {outcome:?}",
|
||||
case.lang,
|
||||
);
|
||||
if let Some(diff) = outcome.differential.as_ref() {
|
||||
assert_ne!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_vuln_fixtures_confirm_via_run_spec() {
|
||||
for case in CASES {
|
||||
let Some(outcome) = run(*case, case.vuln_file, case.vuln_class) else {
|
||||
continue;
|
||||
};
|
||||
assert_confirmed(*case, &outcome);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_benign_fixtures_do_not_confirm_via_run_spec() {
|
||||
for case in CASES {
|
||||
let Some(outcome) = run(*case, case.benign_file, case.benign_class) else {
|
||||
continue;
|
||||
};
|
||||
assert_not_confirmed(*case, &outcome);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_typescript_stages_commonjs_entry_for_stock_node() {
|
||||
let spec = make_spec(Lang::TypeScript);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert_eq!(h.entry_subpath.as_deref(), Some("entry.js"));
|
||||
assert!(h.source.contains("require('./entry')"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_harnesses_emit_sink_hit_sentinel() {
|
||||
for lang in LANGS {
|
||||
let spec = make_spec(*lang);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(
|
||||
h.source.contains("__NYX_SINK_HIT__"),
|
||||
"{lang:?} ClassMethod harness must emit the runner sink sentinel",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
/* Uses execve via fork; the shell never sees `input`. */
|
||||
/* Uses execve via fork; the shell never sees or echoes `input`. */
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
char *argv[] = { (char*)"/bin/echo", (char*)(input ? input : ""), NULL };
|
||||
execv("/bin/echo", argv);
|
||||
char *argv[] = { (char*)"/usr/bin/true", (char*)(input ? input : ""), NULL };
|
||||
execv("/usr/bin/true", argv);
|
||||
_exit(127);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "echo %s", input ? input : "");
|
||||
snprintf(buf, sizeof(buf), "true %s", input ? input : "");
|
||||
/* SINK: tainted input → system(3) */
|
||||
system(buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ public:
|
|||
void run(const std::string& input) {
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
const char* argv[] = { "/bin/echo", input.c_str(), nullptr };
|
||||
execv("/bin/echo", const_cast<char* const*>(argv));
|
||||
const char* argv[] = { "/usr/bin/true", input.c_str(), nullptr };
|
||||
execv("/usr/bin/true", const_cast<char* const*>(argv));
|
||||
_exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class UserService {
|
|||
public:
|
||||
UserService() = default;
|
||||
void run(const std::string& input) {
|
||||
std::string cmd = std::string("echo ") + input;
|
||||
std::string cmd = std::string("true ") + input;
|
||||
// SINK: tainted input → system(3)
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ import "os/exec"
|
|||
type UserService struct{}
|
||||
|
||||
func (UserService) Run(input string) string {
|
||||
out, _ := exec.Command("/bin/echo", input).Output()
|
||||
out, _ := exec.Command("true", input).Output()
|
||||
return string(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ type UserService struct{}
|
|||
|
||||
func (UserService) Run(input string) string {
|
||||
// SINK: tainted input → shell -c
|
||||
out, _ := exec.Command("sh", "-c", "echo "+input).Output()
|
||||
out, _ := exec.Command("sh", "-c", "true "+input).Output()
|
||||
return string(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Java.
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
//
|
||||
// The payload is passed as an argv element to true(1), so no shell parses or
|
||||
// echoes marker bytes.
|
||||
public class Benign {
|
||||
public static class UserRepository {
|
||||
public UserRepository() {}
|
||||
|
||||
public void findByName(String name) throws SQLException {
|
||||
Connection c = DriverManager.getConnection("jdbc:sqlite::memory:");
|
||||
PreparedStatement ps = c.prepareStatement("SELECT id FROM users WHERE name = ?");
|
||||
ps.setString(1, name);
|
||||
ps.execute();
|
||||
ps.close();
|
||||
c.close();
|
||||
public void findByName(String name) throws Exception {
|
||||
Process p = new ProcessBuilder("/usr/bin/true", name)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Java.
|
||||
//
|
||||
// UserRepository.findByName concatenates user input into a JDBC SQL
|
||||
// statement. Default constructor exists so the harness can build the
|
||||
// receiver without stubbing dependencies.
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.Statement;
|
||||
import java.sql.SQLException;
|
||||
// UserRepository.findByName concatenates user input into a shell command.
|
||||
// The nested class has a default constructor so the ClassMethod harness can
|
||||
// build the receiver reflectively.
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Vuln {
|
||||
public static class UserRepository {
|
||||
public UserRepository() {}
|
||||
|
||||
public void findByName(String name) throws SQLException {
|
||||
Connection c = DriverManager.getConnection("jdbc:sqlite::memory:");
|
||||
Statement s = c.createStatement();
|
||||
// SINK: tainted concat into SQL
|
||||
String sql = "SELECT id FROM users WHERE name = '" + name + "'";
|
||||
s.execute(sql);
|
||||
s.close();
|
||||
c.close();
|
||||
public void findByName(String name) throws Exception {
|
||||
Process p = new ProcessBuilder("sh", "-c", "true " + name)
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
try (InputStream in = p.getInputStream()) {
|
||||
in.transferTo(System.out);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for JavaScript.
|
||||
//
|
||||
// UserService.run routes the input through execFileSync with argv form so
|
||||
// the shell never interprets the string.
|
||||
// the shell never interprets the string or echoes marker bytes.
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
return execFileSync('/bin/echo', [input]).toString();
|
||||
return execFileSync('true', [input]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class UserService {
|
|||
constructor() {}
|
||||
run(input) {
|
||||
// SINK: untrusted input → shell
|
||||
return execSync('echo ' + input).toString();
|
||||
return execSync('true ' + input).toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ class UserService {
|
|||
public function __construct() {}
|
||||
|
||||
public function run($input) {
|
||||
return shell_exec('echo ' . escapeshellarg($input));
|
||||
return shell_exec('true ' . escapeshellarg($input));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ class UserService {
|
|||
|
||||
public function run($input) {
|
||||
// SINK: tainted input → shell.
|
||||
return shell_exec('echo ' . $input);
|
||||
return shell_exec('true ' . $input);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ class UserService
|
|||
end
|
||||
|
||||
def run(input)
|
||||
`echo #{Shellwords.escape(input)}`
|
||||
`true #{Shellwords.escape(input)}`
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ class UserService
|
|||
|
||||
def run(input)
|
||||
# SINK: tainted input → shell
|
||||
`echo #{input}`
|
||||
`true #{input}`
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pub struct UserService;
|
|||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
let out = std::process::Command::new("/bin/echo")
|
||||
let out = std::process::Command::new("true")
|
||||
.arg(input)
|
||||
.output()
|
||||
.expect("exec");
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct UserService;
|
|||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
// SINK: tainted input → shell -c
|
||||
let cmd = format!("echo {}", input);
|
||||
let cmd = format!("true {}", input);
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for TypeScript.
|
||||
import { execFileSync } from 'child_process';
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
export class UserService {
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input: string): string {
|
||||
return execFileSync('/bin/echo', [input]).toString();
|
||||
run(input) {
|
||||
return execFileSync('true', [input]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for TypeScript.
|
||||
//
|
||||
// UserService.run forwards user input directly to a shell. Default ctor.
|
||||
import { execSync } from 'child_process';
|
||||
// UserService.run forwards user input directly to a shell. The source
|
||||
// stays CommonJS-compatible because the harness stages TS fixtures as
|
||||
// entry.js for stock Node.
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
export class UserService {
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input: string): string {
|
||||
run(input) {
|
||||
// SINK: untrusted input flows into the shell
|
||||
return execSync('echo ' + input).toString();
|
||||
return execSync('true ' + input).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
// Same route shape as vuln.php, but quotes the payload before invoking
|
||||
// the shell so the command-injection marker remains inert.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function match(array $methods, string $path, array $callable)
|
||||
{
|
||||
\NyxLaravelMultiVerb\register_route($methods, $callable);
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace NyxLaravelMultiVerb {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
function register_route(array $methods, array $callable): void
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if (!in_array($requestMethod, $methods, true)) {
|
||||
return null;
|
||||
}
|
||||
[$class, $method] = $callable;
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
}
|
||||
|
||||
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload)
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . escapeshellarg($payload);
|
||||
$out = shell_exec($cmd);
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
// Laravel-style multi-verb route fixture. The vulnerable sink is gated
|
||||
// to POST so verifier runs that exercise only the representative GET
|
||||
// method miss the command injection.
|
||||
|
||||
namespace Illuminate\Support\Facades {
|
||||
class Route
|
||||
{
|
||||
public static function match(array $methods, string $path, array $callable)
|
||||
{
|
||||
\NyxLaravelMultiVerb\register_route($methods, $callable);
|
||||
return new class {
|
||||
public function middleware($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace NyxLaravelMultiVerb {
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
function register_route(array $methods, array $callable): void
|
||||
{
|
||||
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
if (!in_array($requestMethod, $methods, true)) {
|
||||
return null;
|
||||
}
|
||||
[$class, $method] = $callable;
|
||||
$controller = new $class();
|
||||
return $controller->$method($payload);
|
||||
};
|
||||
}
|
||||
|
||||
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function run(string $payload)
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
echo "__NYX_METHOD_SKIP__\n";
|
||||
return null;
|
||||
}
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$cmd = "true " . $payload;
|
||||
$out = shell_exec($cmd);
|
||||
echo $out;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ mod common;
|
|||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod php_fixture_tests {
|
||||
use crate::common::fixture_harness::FIXTURE_LOCK;
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding};
|
||||
use nyx_scanner::evidence::{
|
||||
|
|
@ -22,11 +23,8 @@ mod php_fixture_tests {
|
|||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn php_available() -> bool {
|
||||
std::process::Command::new("php")
|
||||
.arg("--version")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
mod common;
|
||||
|
||||
use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding};
|
||||
use nyx_scanner::evidence::EntryKind;
|
||||
use nyx_scanner::summary::FuncSummary;
|
||||
|
|
@ -67,6 +69,24 @@ fn laravel_benign_fixture_binds_same_route_shape() {
|
|||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_multi_verb_fixture_preserves_match_methods() {
|
||||
let path = "tests/dynamic_fixtures/php_frameworks/laravel_multi_verb/vuln.php";
|
||||
let bytes = std::fs::read(path).expect("laravel multi-verb fixture exists");
|
||||
let tree = parse_php(&bytes);
|
||||
let summary = summary_for("run", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Php)
|
||||
.expect("laravel adapter must bind multi-verb fixture");
|
||||
assert_eq!(binding.adapter, "php-laravel");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
vec![HttpMethod::GET, HttpMethod::POST]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symfony_vuln_fixture_binds_route_via_attribute() {
|
||||
let path = "tests/dynamic_fixtures/php_frameworks/symfony/vuln.php";
|
||||
|
|
@ -135,3 +155,134 @@ fn laravel_adapter_ignores_helper_method() {
|
|||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Php);
|
||||
assert!(binding.is_none());
|
||||
}
|
||||
|
||||
mod e2e_phase_16_laravel_multi_verb {
|
||||
use super::{common::fixture_harness::FIXTURE_LOCK, parse_php, summary_for};
|
||||
use nyx_scanner::dynamic::framework::{HttpMethod, detect_binding};
|
||||
use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec};
|
||||
use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions};
|
||||
use nyx_scanner::dynamic::spec::{
|
||||
EntryKind, HarnessSpec, JavaToolchain, PayloadSlot, SpecDerivationStrategy,
|
||||
default_toolchain_id,
|
||||
};
|
||||
use nyx_scanner::evidence::DifferentialVerdict;
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn command_available(bin: &str) -> bool {
|
||||
Command::new(bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(file: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/php_frameworks/laravel_multi_verb")
|
||||
.join(file)
|
||||
}
|
||||
|
||||
fn build_spec(file: &str) -> (HarnessSpec, TempDir) {
|
||||
let src = fixture_path(file);
|
||||
let tmp = TempDir::new().expect("create tempdir");
|
||||
let dst = tmp.path().join(file);
|
||||
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
|
||||
let entry_file = dst.to_string_lossy().into_owned();
|
||||
let bytes = std::fs::read(&dst).expect("copied fixture readable");
|
||||
let tree = parse_php(&bytes);
|
||||
let summary = summary_for("run", &entry_file);
|
||||
let framework = detect_binding(&summary, tree.root_node(), &bytes, Lang::Php)
|
||||
.expect("multi-verb Laravel fixture must bind");
|
||||
let route = framework.route.as_ref().expect("route");
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
vec![HttpMethod::GET, HttpMethod::POST],
|
||||
"fixture must exercise GET+POST fanout"
|
||||
);
|
||||
|
||||
let mut digest = blake3::Hasher::new();
|
||||
digest.update(b"phase16-e2e-php-laravel-multi-verb|");
|
||||
digest.update(file.as_bytes());
|
||||
let spec_hash = format!("{:016x}", {
|
||||
let bytes = digest.finalize();
|
||||
u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap())
|
||||
});
|
||||
|
||||
let spec = HarnessSpec {
|
||||
finding_id: spec_hash.clone(),
|
||||
entry_file: entry_file.clone(),
|
||||
entry_name: "run".to_owned(),
|
||||
entry_kind: EntryKind::HttpRoute,
|
||||
lang: Lang::Php,
|
||||
toolchain_id: default_toolchain_id(Lang::Php).to_owned(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::CODE_EXEC,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line: 1,
|
||||
spec_hash,
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: Some(framework),
|
||||
java_toolchain: JavaToolchain::default(),
|
||||
};
|
||||
(spec, tmp)
|
||||
}
|
||||
|
||||
fn run(file: &str) -> Option<RunOutcome> {
|
||||
if !command_available("php") {
|
||||
eprintln!("SKIP laravel_multi_verb/{file}: missing php");
|
||||
return None;
|
||||
}
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (spec, _tmp) = build_spec(file);
|
||||
let opts = SandboxOptions {
|
||||
backend: SandboxBackend::Process,
|
||||
..SandboxOptions::default()
|
||||
};
|
||||
match run_spec(&spec, &opts) {
|
||||
Ok(outcome) => Some(outcome),
|
||||
Err(RunError::BuildFailed { stderr, attempts }) => {
|
||||
eprintln!(
|
||||
"SKIP laravel_multi_verb/{file}: harness build failed after {attempts} attempts: {stderr}",
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => panic!("run_spec(laravel_multi_verb/{file}) errored: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_match_post_branch_confirms_via_run_spec() {
|
||||
let Some(outcome) = run("vuln.php") else {
|
||||
return;
|
||||
};
|
||||
assert!(
|
||||
outcome.triggered_by.is_some(),
|
||||
"Laravel Route::match vuln must Confirm via POST fanout; got {outcome:?}",
|
||||
);
|
||||
let diff = outcome
|
||||
.differential
|
||||
.as_ref()
|
||||
.expect("confirmed run must carry differential outcome");
|
||||
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_match_benign_does_not_confirm_via_run_spec() {
|
||||
let Some(outcome) = run("benign.php") else {
|
||||
return;
|
||||
};
|
||||
assert!(
|
||||
outcome.triggered_by.is_none(),
|
||||
"Laravel Route::match benign control must not Confirm; got {outcome:?}",
|
||||
);
|
||||
if let Some(diff) = &outcome.differential {
|
||||
assert_ne!(diff.verdict, DifferentialVerdict::Confirmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue