mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 19: Track M.1 — ClassMethod end-to-end (all langs)
This commit is contained in:
parent
1b2f9cb7ca
commit
b374f89577
35 changed files with 1894 additions and 41 deletions
|
|
@ -44,6 +44,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::LibraryApi,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||
|
|
@ -438,6 +439,14 @@ fn c_string_literal(s: &str) -> String {
|
|||
|
||||
/// Emit a C harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. C has no class
|
||||
// system — the dispatcher treats `class` + `method` as a single
|
||||
// free function whose name is the entry symbol (often
|
||||
// `Class_method` by convention) and calls it with the payload.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
}
|
||||
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
match (&spec.payload_slot, shape) {
|
||||
|
|
@ -458,6 +467,58 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for C.
|
||||
///
|
||||
/// C has no classes; the dispatcher calls the conventional
|
||||
/// `<class>_<method>(const char *payload, size_t len)` free function
|
||||
/// the fixture declares. When the fixture exposes a different
|
||||
/// symbol shape the caller is expected to pre-rewrite the
|
||||
/// `entry_name` field; this fallback keeps the build path uniform
|
||||
/// for the Phase 19 acceptance harness even though the class /
|
||||
/// method projection collapses to a free-function call in C.
|
||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let symbol = format!("{class}_{method}");
|
||||
let body = format!(
|
||||
r#"/* Nyx dynamic harness — class method (Phase 19 / Track M.1). */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
{shim}
|
||||
static char *nyx_payload(void);
|
||||
|
||||
#include "entry.c"
|
||||
|
||||
int main(int argc, char *argv[]) {{
|
||||
(void)argc; (void)argv;
|
||||
char *payload = nyx_payload();
|
||||
if (!payload) payload = (char*)"";
|
||||
__nyx_install_crash_guard("{symbol}");
|
||||
{symbol}(payload, strlen(payload));
|
||||
return 0;
|
||||
}}
|
||||
|
||||
static char *nyx_payload(void) {{
|
||||
const char *v = getenv("NYX_PAYLOAD");
|
||||
if (v && *v) {{
|
||||
return strdup(v);
|
||||
}}
|
||||
return strdup("");
|
||||
}}
|
||||
"#,
|
||||
symbol = symbol,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "main.c".into(),
|
||||
command: vec!["./nyx_harness".into()],
|
||||
extra_files: vec![("Makefile".into(), generate_makefile())],
|
||||
entry_subpath: Some("entry.c".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the harness `main.c` for the resolved shape.
|
||||
fn generate_main_c(spec: &HarnessSpec, shape: CShape) -> String {
|
||||
let invocation = invoke_for_shape(spec, shape);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::LibraryApi,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||
|
|
@ -390,6 +391,15 @@ fn cpp_string_literal(s: &str) -> String {
|
|||
|
||||
/// Emit a C++ harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. The harness
|
||||
// constructs the receiver via its default constructor and invokes
|
||||
// `method(payload)`. Fixtures are expected to expose a default
|
||||
// constructor; the fallback path lets the harness build by
|
||||
// null-filling primitive formals when the default ctor is missing.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
}
|
||||
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
match (&spec.payload_slot, shape) {
|
||||
|
|
@ -410,6 +420,54 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for C++.
|
||||
///
|
||||
/// Includes `entry.cpp`, constructs the class via the default
|
||||
/// constructor (`<class> instance;`), and calls
|
||||
/// `instance.<method>(payload)`.
|
||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let body = format!(
|
||||
r#"// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
{shim}
|
||||
static std::string nyx_payload();
|
||||
|
||||
#include "entry.cpp"
|
||||
|
||||
int main(int argc, char *argv[]) {{
|
||||
(void)argc; (void)argv;
|
||||
std::string payload = nyx_payload();
|
||||
__nyx_install_crash_guard("{class}::{method}");
|
||||
{class} instance;
|
||||
instance.{method}(payload);
|
||||
return 0;
|
||||
}}
|
||||
|
||||
static std::string nyx_payload() {{
|
||||
if (const char *v = std::getenv("NYX_PAYLOAD")) {{
|
||||
if (*v) return std::string(v);
|
||||
}}
|
||||
return std::string();
|
||||
}}
|
||||
"#,
|
||||
class = class,
|
||||
method = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "main.cpp".into(),
|
||||
command: vec!["./nyx_harness".into()],
|
||||
extra_files: vec![("CMakeLists.txt".into(), generate_cmake())],
|
||||
entry_subpath: Some("entry.cpp".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_main_cpp(spec: &HarnessSpec, shape: CppShape) -> String {
|
||||
let invocation = invoke_for_shape(spec, shape);
|
||||
let (entry_open, entry_close) = entry_include_guards(spec);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for GoEmitter {
|
||||
|
|
@ -571,6 +572,17 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Go has no
|
||||
// classes — the dispatcher treats `class` as a top-level struct
|
||||
// declared in the entry file and `method` as a method on its
|
||||
// value or pointer receiver. The harness instantiates a zero
|
||||
// value (`var v entry.Class`) and invokes `v.Method(payload)` via
|
||||
// reflection so an unexported method on a pointer receiver still
|
||||
// dispatches.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = GoShape::detect(spec, &entry_source);
|
||||
let main_go = generate_main_go(spec, shape);
|
||||
|
|
@ -1024,6 +1036,99 @@ fn generate_go_mod() -> String {
|
|||
"module nyx-harness\n\ngo 1.21\n".to_owned()
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Go.
|
||||
///
|
||||
/// `class` is mapped to a struct type declared in `entry/entry.go`
|
||||
/// and `method` to a method-on-receiver. The harness uses reflection
|
||||
/// to construct a zero value, then invokes the method with the
|
||||
/// payload — supporting both value and pointer receivers.
|
||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let go_mod = generate_go_mod();
|
||||
let source = format!(
|
||||
r##"// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"nyx-harness/entry"
|
||||
)
|
||||
|
||||
{shim}
|
||||
|
||||
func nyxBuildReceiver(structName string) (reflect.Value, error) {{
|
||||
// Look up the exported type by name on the entry package. Go's
|
||||
// reflect API does not expose package-level reflection over types
|
||||
// directly, so the dispatcher uses the package's well-known
|
||||
// `NyxReceivers` registry the entry file is expected to publish.
|
||||
if r, ok := entry.NyxReceivers[structName]; ok {{
|
||||
return reflect.ValueOf(r), nil
|
||||
}}
|
||||
return reflect.Value{{}}, fmt.Errorf("class not found: %s", structName)
|
||||
}}
|
||||
|
||||
func nyxPayload() string {{
|
||||
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
|
||||
return v
|
||||
}}
|
||||
return ""
|
||||
}}
|
||||
|
||||
func main() {{
|
||||
payload := nyxPayload()
|
||||
__nyx_install_crash_guard("{class}.{method}")
|
||||
v, err := nyxBuildReceiver("{class}")
|
||||
if err != nil {{
|
||||
fmt.Fprintln(os.Stderr, "NYX_CLASS_NOT_FOUND: "+"{class}")
|
||||
os.Exit(78)
|
||||
}}
|
||||
m := v.MethodByName("{method}")
|
||||
if !m.IsValid() {{
|
||||
// reflect.ValueOf(receiver) returns a non-addressable Value, so
|
||||
// v.CanAddr() is always false. Promote to an addressable copy
|
||||
// via reflect.New so pointer-receiver methods bind.
|
||||
ptr := reflect.New(v.Type())
|
||||
ptr.Elem().Set(v)
|
||||
m = ptr.MethodByName("{method}")
|
||||
}}
|
||||
if !m.IsValid() {{
|
||||
fmt.Fprintln(os.Stderr, "NYX_METHOD_NOT_FOUND: "+"{method}")
|
||||
os.Exit(78)
|
||||
}}
|
||||
defer func() {{
|
||||
if r := recover(); r != nil {{
|
||||
fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r)
|
||||
}}
|
||||
}}()
|
||||
args := make([]reflect.Value, m.Type().NumIn())
|
||||
for i := 0; i < m.Type().NumIn(); i++ {{
|
||||
if m.Type().In(i).Kind() == reflect.String {{
|
||||
args[i] = reflect.ValueOf(payload)
|
||||
}} else {{
|
||||
args[i] = reflect.Zero(m.Type().In(i))
|
||||
}}
|
||||
}}
|
||||
out := m.Call(args)
|
||||
if len(out) > 0 {{
|
||||
fmt.Println(out[0].Interface())
|
||||
}}
|
||||
}}
|
||||
"##,
|
||||
class = class,
|
||||
method = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source,
|
||||
filename: "main.go".to_owned(),
|
||||
command: vec!["./nyx_harness".to_owned()],
|
||||
extra_files: vec![("go.mod".to_owned(), go_mod)],
|
||||
entry_subpath: Some("entry/entry.go".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal `gin` stub package used by [`GoShape::GinHandler`] fixtures
|
||||
/// so the toolchain can compile without a real gin dependency.
|
||||
/// Exposes just enough surface (Context.Query, Context.JSON,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for JavaEmitter {
|
||||
|
|
@ -590,6 +591,16 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Routes through
|
||||
// the existing `invokeReflective` helper so the harness instantiates
|
||||
// the receiver via its no-arg constructor (or null-fills primitive
|
||||
// / null-safe-object formals) before dispatching `method(payload)`.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let entry_class = derive_entry_class(&entry_source);
|
||||
return Ok(emit_class_method_harness(spec, class, method, &entry_class));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = JavaShape::detect(spec, &entry_source);
|
||||
let entry_class = derive_entry_class(&entry_source);
|
||||
|
|
@ -1780,6 +1791,152 @@ const REFLECTIVE_HELPER: &str = r#"
|
|||
}
|
||||
"#;
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Java.
|
||||
///
|
||||
/// Emits a `NyxHarness.java` whose `main` reflectively constructs the
|
||||
/// target class via its no-arg constructor (when available) — or
|
||||
/// fills primitive parameters with defaults + object parameters with
|
||||
/// the Phase 19 [`crate::dynamic::stubs::MockKind`] doubles when the
|
||||
/// no-arg path is missing — and invokes `method(payload)`. The class
|
||||
/// is loaded via the same FQN qualifier used by the regular Java
|
||||
/// shapes so it works on both default-package fixtures and packaged
|
||||
/// OWASP-style entries.
|
||||
fn emit_class_method_harness(
|
||||
spec: &HarnessSpec,
|
||||
class: &str,
|
||||
method: &str,
|
||||
entry_class: &str,
|
||||
) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let pre_call = pre_call_setup(spec);
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
crate::symbol::Lang::Java,
|
||||
);
|
||||
let mock_db = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::DatabaseConnection,
|
||||
crate::symbol::Lang::Java,
|
||||
);
|
||||
let mock_log = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::Logger,
|
||||
crate::symbol::Lang::Java,
|
||||
);
|
||||
let source = format!(
|
||||
r#"// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class NyxHarness {{
|
||||
{probe}
|
||||
|
||||
{mock_http}
|
||||
{mock_db}
|
||||
{mock_log}
|
||||
|
||||
static Object nyxBuildReceiver(Class<?> cls) throws Exception {{
|
||||
// Preferred path: zero-arg ctor.
|
||||
try {{
|
||||
Constructor<?> c = cls.getDeclaredConstructor();
|
||||
c.setAccessible(true);
|
||||
return c.newInstance();
|
||||
}} catch (NoSuchMethodException ignore) {{
|
||||
}}
|
||||
// Fallback path: walk declared ctors and stub each formal.
|
||||
for (Constructor<?> c : cls.getDeclaredConstructors()) {{
|
||||
c.setAccessible(true);
|
||||
Class<?>[] params = c.getParameterTypes();
|
||||
Object[] args = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {{
|
||||
args[i] = nyxStubForType(params[i]);
|
||||
}}
|
||||
try {{ return c.newInstance(args); }} catch (Exception ignore) {{}}
|
||||
}}
|
||||
return null;
|
||||
}}
|
||||
|
||||
static Object nyxStubForType(Class<?> t) {{
|
||||
String n = t.getName().toLowerCase();
|
||||
if (n.contains("http") || n.contains("client")) return new MockHttpClient();
|
||||
if (n.contains("database") || n.contains("connection") || n.contains("session") || n.contains("repository")) return new MockDatabaseConnection();
|
||||
if (n.contains("logger") || n.contains("log")) return new MockLogger();
|
||||
if (t.equals(String.class)) return "";
|
||||
if (t.equals(int.class) || t.equals(Integer.class)) return 0;
|
||||
if (t.equals(long.class) || t.equals(Long.class)) return 0L;
|
||||
if (t.equals(boolean.class) || t.equals(Boolean.class)) return false;
|
||||
return null;
|
||||
}}
|
||||
|
||||
public static void main(String[] args) {{
|
||||
String payload = nyxPayload();
|
||||
{pre_call} try {{
|
||||
Class<?> cls;
|
||||
try {{
|
||||
cls = Class.forName({class_fqn:?});
|
||||
}} catch (ClassNotFoundException cnfe) {{
|
||||
cls = Class.forName({entry_class_fqn:?});
|
||||
}}
|
||||
Object instance = nyxBuildReceiver(cls);
|
||||
if (instance == null) {{
|
||||
System.err.println("NYX_CLASS_CTOR_FAILED: " + cls.getName());
|
||||
System.exit(78);
|
||||
}}
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {{
|
||||
if (m.getName().equals({method:?})) {{ match = m; break; }}
|
||||
}}
|
||||
if (match == null) {{
|
||||
System.err.println("NYX_METHOD_NOT_FOUND: " + {method:?});
|
||||
System.exit(78);
|
||||
}}
|
||||
match.setAccessible(true);
|
||||
Class<?>[] params = match.getParameterTypes();
|
||||
Object[] mArgs = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {{
|
||||
mArgs[i] = params[i].equals(String.class) ? payload : nyxStubForType(params[i]);
|
||||
}}
|
||||
match.invoke(instance, mArgs);
|
||||
}} catch (InvocationTargetException ite) {{
|
||||
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
|
||||
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());
|
||||
}} catch (Throwable e) {{
|
||||
System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
}}
|
||||
}}
|
||||
|
||||
static String nyxPayload() {{
|
||||
String v = System.getenv("NYX_PAYLOAD");
|
||||
if (v != null && !v.isEmpty()) {{
|
||||
return v;
|
||||
}}
|
||||
String b64 = System.getenv("NYX_PAYLOAD_B64");
|
||||
if (b64 != null && !b64.isEmpty()) {{
|
||||
byte[] decoded = java.util.Base64.getDecoder().decode(b64);
|
||||
return new String(decoded, java.nio.charset.StandardCharsets.UTF_8);
|
||||
}}
|
||||
return "";
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
class_fqn = class,
|
||||
entry_class_fqn = entry_class,
|
||||
method = method,
|
||||
pre_call = pre_call,
|
||||
);
|
||||
HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
command: vec![
|
||||
"java".to_owned(),
|
||||
"-cp".to_owned(),
|
||||
".".to_owned(),
|
||||
"NyxHarness".to_owned(),
|
||||
],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some(format!("{entry_class}.java")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reflective JUnit-shape invocation. Reads the payload from
|
||||
/// `NYX_PAYLOAD` (no method argument) — JUnit tests typically capture
|
||||
/// inputs through fields or `System.getenv`.
|
||||
|
|
|
|||
|
|
@ -567,6 +567,14 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
|
|||
return Ok(emit_prototype_pollution_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Same shape gap
|
||||
// closer as the Python emitter — instantiate the class via its
|
||||
// zero-arg constructor (falling back to a stubbed-dependency ctor
|
||||
// when the zero-arg path throws) and invoke `method(payload)`.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method(spec, class, method, is_typescript));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = JsShape::detect(spec, &entry_source);
|
||||
let entry_subpath = entry_subpath_for_shape(shape, is_typescript);
|
||||
|
|
@ -581,6 +589,111 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Node.js / TypeScript.
|
||||
///
|
||||
/// Imports the entry module, locates `class` on the exported surface,
|
||||
/// instantiates via the default constructor, falls back to a single
|
||||
/// mock-dependency ctor when the zero-arg path throws, and invokes
|
||||
/// `instance[method](payload)`.
|
||||
fn emit_class_method(
|
||||
_spec: &HarnessSpec,
|
||||
class: &str,
|
||||
method: &str,
|
||||
is_typescript: bool,
|
||||
) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let entry_subpath = if is_typescript { "entry.ts" } else { "entry.js" };
|
||||
let entry_require_path = entry_require_path(entry_subpath);
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
crate::symbol::Lang::JavaScript,
|
||||
);
|
||||
let mock_db = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::DatabaseConnection,
|
||||
crate::symbol::Lang::JavaScript,
|
||||
);
|
||||
let mock_log = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::Logger,
|
||||
crate::symbol::Lang::JavaScript,
|
||||
);
|
||||
let body = format!(
|
||||
r#"'use strict';
|
||||
// Nyx dynamic harness — class method (Phase 19 / Track M.1), auto-generated.
|
||||
{probe}
|
||||
|
||||
{mock_http}
|
||||
{mock_db}
|
||||
{mock_log}
|
||||
|
||||
const payload = (process.env.NYX_PAYLOAD && process.env.NYX_PAYLOAD.length > 0)
|
||||
? process.env.NYX_PAYLOAD
|
||||
: (process.env.NYX_PAYLOAD_B64
|
||||
? Buffer.from(process.env.NYX_PAYLOAD_B64, 'base64').toString('utf8')
|
||||
: '');
|
||||
|
||||
let _entry;
|
||||
try {{
|
||||
_entry = require('./{entry_require_path}');
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
|
||||
process.exit(77);
|
||||
}}
|
||||
|
||||
const _Cls = _entry[{class:?}] || (_entry.default && _entry.default[{class:?}]) || (typeof _entry.default === 'function' && _entry.default.name === {class:?} ? _entry.default : null);
|
||||
if (typeof _Cls !== 'function') {{
|
||||
process.stderr.write('NYX_CLASS_NOT_FOUND: ' + {class:?} + '\n');
|
||||
process.exit(78);
|
||||
}}
|
||||
|
||||
function _nyxBuildReceiver(Cls) {{
|
||||
try {{
|
||||
return new Cls();
|
||||
}} catch (_e) {{
|
||||
// Fall back to a single mock-dependency ctor. The brief allows
|
||||
// up to depth-3 dependency stubbing; v1 keeps the chain depth
|
||||
// at one and lets the verifier promote precision in a later
|
||||
// phase.
|
||||
try {{ return new Cls(new MockHttpClient(), new MockDatabaseConnection(), new MockLogger()); }} catch (_e2) {{}}
|
||||
try {{ return new Cls(new MockDatabaseConnection()); }} catch (_e3) {{}}
|
||||
try {{ return new Cls(new MockHttpClient()); }} catch (_e4) {{}}
|
||||
try {{ return new Cls(new MockLogger()); }} catch (_e5) {{}}
|
||||
return null;
|
||||
}}
|
||||
}}
|
||||
|
||||
const _instance = _nyxBuildReceiver(_Cls);
|
||||
if (_instance == null) {{
|
||||
process.stderr.write('NYX_CLASS_CTOR_FAILED: ' + {class:?} + '\n');
|
||||
process.exit(78);
|
||||
}}
|
||||
|
||||
const _m = _instance[{method:?}];
|
||||
if (typeof _m !== 'function') {{
|
||||
process.stderr.write('NYX_METHOD_NOT_FOUND: ' + {method:?} + '\n');
|
||||
process.exit(78);
|
||||
}}
|
||||
|
||||
(async () => {{
|
||||
try {{
|
||||
const _result = await Promise.resolve(_m.call(_instance, payload));
|
||||
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');
|
||||
}}
|
||||
}})();
|
||||
"#,
|
||||
class = class,
|
||||
method = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.js".to_owned(),
|
||||
command: vec!["node".to_owned(), "harness.js".to_owned()],
|
||||
extra_files: Vec::new(),
|
||||
entry_subpath: Some(entry_subpath.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 04 — Track J.2 SSTI harness for Node (Handlebars).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, simulates Handlebars's `{{helper a b}}`
|
||||
|
|
@ -1634,6 +1747,7 @@ pub const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::LibraryApi,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -394,17 +394,16 @@ mod tests {
|
|||
assert_eq!(EntryKind::Unknown.tag(), T::Unknown);
|
||||
}
|
||||
|
||||
/// Phase 18 (Track M.0) — none of the Phase 18 variants are wired
|
||||
/// into any per-language emitter yet (those land in Phase 19 /
|
||||
/// 20 / 21). Confirm every lang routes them through the
|
||||
/// Phase 18 (Track M.0) baseline — the Phase 18 variants not yet
|
||||
/// wired by a follow-up phase still route through the
|
||||
/// supported-set gate so the verifier produces a structured
|
||||
/// `Inconclusive(EntryKindUnsupported)` rather than degrading
|
||||
/// silently.
|
||||
/// silently. Phase 19 lands `ClassMethod`, so it is excluded
|
||||
/// from the still-unsupported set.
|
||||
#[test]
|
||||
fn entry_kind_phase_18_variants_are_unsupported_everywhere() {
|
||||
fn entry_kind_phase_20_21_variants_are_unsupported_everywhere() {
|
||||
use crate::evidence::EntryKindTag as T;
|
||||
let new = [
|
||||
T::ClassMethod,
|
||||
let still_unsupported = [
|
||||
T::MessageHandler,
|
||||
T::ScheduledJob,
|
||||
T::GraphQLResolver,
|
||||
|
|
@ -425,10 +424,10 @@ mod tests {
|
|||
Lang::Cpp,
|
||||
] {
|
||||
let supported = entry_kinds_supported(lang);
|
||||
for tag in new {
|
||||
for tag in still_unsupported {
|
||||
assert!(
|
||||
!supported.contains(&tag),
|
||||
"{lang:?} prematurely advertised {tag:?} — Phase 18 keeps the new variants unsupported until Phase 19 / 20 / 21 lands the per-lang adapters"
|
||||
"{lang:?} prematurely advertised {tag:?} — Phase 20 / 21 has not landed the per-lang adapters for this variant"
|
||||
);
|
||||
let hint = entry_kind_hint(lang, tag);
|
||||
assert!(
|
||||
|
|
@ -438,4 +437,30 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — every lang emitter now advertises
|
||||
/// `ClassMethod` so the verifier dispatches structurally instead
|
||||
/// of degrading to `Inconclusive(EntryKindUnsupported)`.
|
||||
#[test]
|
||||
fn entry_kind_class_method_supported_everywhere_after_phase_19() {
|
||||
use crate::evidence::EntryKindTag as T;
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::Rust,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Go,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
] {
|
||||
let supported = entry_kinds_supported(lang);
|
||||
assert!(
|
||||
supported.contains(&T::ClassMethod),
|
||||
"{lang:?} must advertise ClassMethod after Phase 19; got {supported:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for PhpEmitter {
|
||||
|
|
@ -489,6 +490,11 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = PhpShape::detect(spec, &entry_source);
|
||||
let source = generate_source(spec, shape);
|
||||
|
|
@ -1139,6 +1145,104 @@ fn build_entry_block(_shape: PhpShape) -> String {
|
|||
.to_owned()
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for PHP.
|
||||
///
|
||||
/// Includes the entry file, instantiates the class via its default
|
||||
/// constructor (`new $class()`), falls back to a single mock-dependency
|
||||
/// ctor when the zero-arg path throws, then invokes
|
||||
/// `$instance->method($payload)`.
|
||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
crate::symbol::Lang::Php,
|
||||
);
|
||||
let mock_db = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::DatabaseConnection,
|
||||
crate::symbol::Lang::Php,
|
||||
);
|
||||
let mock_log = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::Logger,
|
||||
crate::symbol::Lang::Php,
|
||||
);
|
||||
let body = format!(
|
||||
r#"<?php
|
||||
// Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
{shim}
|
||||
{mock_http}
|
||||
{mock_db}
|
||||
{mock_log}
|
||||
|
||||
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
|
||||
$_b64 = getenv('NYX_PAYLOAD_B64');
|
||||
if ((!$payload || $payload === '') && is_string($_b64) && $_b64 !== '') {{
|
||||
$decoded = base64_decode($_b64, true);
|
||||
if ($decoded !== false) $payload = $decoded;
|
||||
}}
|
||||
|
||||
try {{
|
||||
require_once __DIR__ . '/entry.php';
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_IMPORT_ERROR: ' . $e->getMessage() . "\n");
|
||||
exit(77);
|
||||
}}
|
||||
|
||||
function _nyx_build_receiver(string $cls) {{
|
||||
if (!class_exists($cls)) return null;
|
||||
try {{ return new $cls(); }} catch (Throwable $e) {{}}
|
||||
$rc = new ReflectionClass($cls);
|
||||
$ctor = $rc->getConstructor();
|
||||
if ($ctor === null) {{
|
||||
try {{ return $rc->newInstanceWithoutConstructor(); }} catch (Throwable $e) {{}}
|
||||
return null;
|
||||
}}
|
||||
$args = [];
|
||||
foreach ($ctor->getParameters() as $p) {{
|
||||
$n = strtolower($p->getName());
|
||||
if (strpos($n, 'http') !== false || strpos($n, 'client') !== false) {{
|
||||
$args[] = new MockHttpClient();
|
||||
}} elseif (strpos($n, 'db') !== false || strpos($n, 'conn') !== false || strpos($n, 'repo') !== false || strpos($n, 'session') !== false) {{
|
||||
$args[] = new MockDatabaseConnection();
|
||||
}} elseif (strpos($n, 'log') !== false) {{
|
||||
$args[] = new MockLogger();
|
||||
}} else {{
|
||||
$args[] = null;
|
||||
}}
|
||||
}}
|
||||
try {{ return $rc->newInstanceArgs($args); }} catch (Throwable $e) {{}}
|
||||
return null;
|
||||
}}
|
||||
|
||||
$instance = _nyx_build_receiver({class_lit:?});
|
||||
if ($instance === null) {{
|
||||
fwrite(STDERR, "NYX_CLASS_CTOR_FAILED: " . {class_lit:?} . "\n");
|
||||
exit(78);
|
||||
}}
|
||||
if (!method_exists($instance, {method_lit:?})) {{
|
||||
fwrite(STDERR, "NYX_METHOD_NOT_FOUND: " . {method_lit:?} . "\n");
|
||||
exit(78);
|
||||
}}
|
||||
try {{
|
||||
$result = call_user_func([$instance, {method_lit:?}], $payload);
|
||||
if ($result !== null) {{
|
||||
echo $result . "\n";
|
||||
}}
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||
}}
|
||||
"#,
|
||||
class_lit = class,
|
||||
method_lit = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.php".to_owned(),
|
||||
command: vec!["php".to_owned(), "harness.php".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some("entry.php".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
|
||||
match shape {
|
||||
PhpShape::TopLevelScript => "null".to_owned(),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for PythonEmitter {
|
||||
|
|
@ -679,6 +680,17 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. When the spec's
|
||||
// entry_kind is the data-bearing `ClassMethod { class, method }`
|
||||
// variant the harness instantiates the class via its default
|
||||
// constructor (falling back to a single mock-dependency argument
|
||||
// when the constructor refuses zero args) and invokes the method
|
||||
// with the payload. The dispatch never reaches the per-shape
|
||||
// generator below.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method(spec, class, method));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = PythonShape::detect(spec, &entry_source);
|
||||
let body = generate_for_shape(spec, shape);
|
||||
|
|
@ -692,6 +704,107 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Python.
|
||||
///
|
||||
/// Imports the entry module, locates `class`, instantiates the
|
||||
/// receiver via the default constructor (preferred path), and invokes
|
||||
/// `method(payload)`. When the default constructor raises a
|
||||
/// `TypeError` (missing positional args), the harness falls back to a
|
||||
/// single mock dependency drawn from [`crate::dynamic::stubs::mocks`]
|
||||
/// — covering the typical controller-needs-service / service-needs-
|
||||
/// repository injection shape Phase 19's brief calls out.
|
||||
fn emit_class_method(spec: &HarnessSpec, class: &str, method: &str) -> HarnessSource {
|
||||
let preamble = harness_preamble(spec);
|
||||
let postamble = harness_postamble();
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
crate::symbol::Lang::Python,
|
||||
);
|
||||
let mock_db = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::DatabaseConnection,
|
||||
crate::symbol::Lang::Python,
|
||||
);
|
||||
let mock_log = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::Logger,
|
||||
crate::symbol::Lang::Python,
|
||||
);
|
||||
let body = format!(
|
||||
r#"# Shape: class method — instantiate receiver, invoke method(payload).
|
||||
{mock_http}
|
||||
{mock_db}
|
||||
{mock_log}
|
||||
|
||||
_cls = getattr(_entry_mod, {class:?}, None)
|
||||
if _cls is None:
|
||||
print("NYX_CLASS_NOT_FOUND: " + {class:?}, file=sys.stderr, flush=True)
|
||||
sys.exit(78)
|
||||
|
||||
def _nyx_build_receiver(cls):
|
||||
# Preferred path: zero-arg ctor.
|
||||
try:
|
||||
return cls()
|
||||
except TypeError:
|
||||
pass
|
||||
# Fallback path: stubbed dependencies. Walk the ctor's positional
|
||||
# formals (best-effort via inspect.signature) and pass mocks for
|
||||
# known shapes; default to `None` for the rest.
|
||||
import inspect
|
||||
try:
|
||||
sig = inspect.signature(cls.__init__)
|
||||
args = []
|
||||
for name, p in list(sig.parameters.items())[1:]: # skip `self`
|
||||
n = name.lower()
|
||||
if 'http' in n or 'client' in n:
|
||||
args.append(MockHttpClient())
|
||||
elif 'db' in n or 'conn' in n or 'session' in n:
|
||||
args.append(MockDatabaseConnection())
|
||||
elif 'log' in n:
|
||||
args.append(MockLogger())
|
||||
else:
|
||||
args.append(None)
|
||||
return cls(*args)
|
||||
except Exception as _e:
|
||||
# Last resort: single-mock fallback so a single-arg ctor still
|
||||
# constructs.
|
||||
try:
|
||||
return cls(MockHttpClient())
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
_instance = _nyx_build_receiver(_cls)
|
||||
if _instance is None:
|
||||
print("NYX_CLASS_CTOR_FAILED: " + {class:?}, file=sys.stderr, flush=True)
|
||||
sys.exit(78)
|
||||
|
||||
try:
|
||||
_m = getattr(_instance, {method:?}, None)
|
||||
if _m is None:
|
||||
print("NYX_METHOD_NOT_FOUND: " + {method:?}, file=sys.stderr, flush=True)
|
||||
sys.exit(78)
|
||||
_result = _m(payload)
|
||||
if _result is not None:
|
||||
try:
|
||||
print(str(_result), flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
except SystemExit as _e:
|
||||
sys.exit(_e.code)
|
||||
except Exception as _e:
|
||||
print(f"NYX_EXCEPTION: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
|
||||
"#,
|
||||
class = class,
|
||||
method = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source: format!("{preamble}\n{body}\n{postamble}"),
|
||||
filename: "harness.py".to_owned(),
|
||||
command: vec!["python3".to_owned(), "harness.py".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 03 — Track J.1 deserialize harness for Python.
|
||||
///
|
||||
/// Reads the payload (`NYX_GADGET_CLASS:<class>`), constructs a
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::Function,
|
||||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for RubyEmitter {
|
||||
|
|
@ -431,6 +432,11 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(class, method));
|
||||
}
|
||||
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = RubyShape::detect(spec, &entry_source);
|
||||
let source = generate_source(spec, shape);
|
||||
|
|
@ -444,6 +450,110 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Ruby.
|
||||
///
|
||||
/// Requires the entry file, looks up `class` as a top-level constant,
|
||||
/// instantiates via `.new` (falling back to a single mock-dependency
|
||||
/// `.new(...)` when the no-arg path raises `ArgumentError`), and
|
||||
/// invokes `instance.send(method, payload)`.
|
||||
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let mock_http = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::HttpClient,
|
||||
crate::symbol::Lang::Ruby,
|
||||
);
|
||||
let mock_db = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::DatabaseConnection,
|
||||
crate::symbol::Lang::Ruby,
|
||||
);
|
||||
let mock_log = crate::dynamic::stubs::mock_source(
|
||||
crate::dynamic::stubs::MockKind::Logger,
|
||||
crate::symbol::Lang::Ruby,
|
||||
);
|
||||
let body = format!(
|
||||
r#"# Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
{shim}
|
||||
{mock_http}
|
||||
{mock_db}
|
||||
{mock_log}
|
||||
|
||||
def nyx_payload
|
||||
v = ENV['NYX_PAYLOAD']
|
||||
return v if v && !v.empty?
|
||||
b64 = ENV['NYX_PAYLOAD_B64']
|
||||
if b64 && !b64.empty?
|
||||
begin
|
||||
require 'base64'
|
||||
return Base64.decode64(b64)
|
||||
rescue StandardError
|
||||
return ''
|
||||
end
|
||||
end
|
||||
''
|
||||
end
|
||||
|
||||
$nyx_payload = nyx_payload
|
||||
|
||||
begin
|
||||
require_relative './entry'
|
||||
rescue LoadError, ScriptError => e
|
||||
STDERR.puts("NYX_IMPORT_ERROR: #{{e.message}}")
|
||||
exit 77
|
||||
end
|
||||
|
||||
cls_name = {class:?}
|
||||
unless Object.const_defined?(cls_name)
|
||||
STDERR.puts("NYX_CLASS_NOT_FOUND: #{{cls_name}}")
|
||||
exit 78
|
||||
end
|
||||
cls = Object.const_get(cls_name)
|
||||
|
||||
def _nyx_build_receiver(cls)
|
||||
begin
|
||||
return cls.new
|
||||
rescue ArgumentError
|
||||
end
|
||||
begin
|
||||
return cls.new(MockHttpClient.new, MockDatabaseConnection.new, MockLogger.new)
|
||||
rescue StandardError
|
||||
end
|
||||
[MockDatabaseConnection.new, MockHttpClient.new, MockLogger.new, nil].each do |dep|
|
||||
begin
|
||||
return cls.new(dep)
|
||||
rescue StandardError
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
instance = _nyx_build_receiver(cls)
|
||||
if instance.nil?
|
||||
STDERR.puts("NYX_CLASS_CTOR_FAILED: #{{cls_name}}")
|
||||
exit 78
|
||||
end
|
||||
unless instance.respond_to?({method:?})
|
||||
STDERR.puts("NYX_METHOD_NOT_FOUND: " + {method:?})
|
||||
exit 78
|
||||
end
|
||||
begin
|
||||
result = instance.send({method:?}, $nyx_payload)
|
||||
print(result.to_s) if result
|
||||
rescue StandardError => e
|
||||
STDERR.puts("NYX_EXCEPTION: #{{e.class.name}}: #{{e.message}}")
|
||||
end
|
||||
"#,
|
||||
class = class,
|
||||
method = method,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "harness.rb".to_owned(),
|
||||
command: vec!["ruby".to_owned(), "harness.rb".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some("entry.rb".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 03 — Track J.1 deserialize harness for Ruby.
|
||||
///
|
||||
/// Wraps a call to `Marshal.load(input)` with a const-lookup
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const SUPPORTED: &[EntryKindTag] = &[
|
|||
EntryKindTag::HttpRoute,
|
||||
EntryKindTag::CliSubcommand,
|
||||
EntryKindTag::LibraryApi,
|
||||
EntryKindTag::ClassMethod,
|
||||
];
|
||||
|
||||
impl LangEmitter for RustEmitter {
|
||||
|
|
@ -818,6 +819,16 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
return Ok(emit_open_redirect_harness(spec));
|
||||
}
|
||||
|
||||
// Phase 19 (Track M.1): ClassMethod short-circuit. Rust has no
|
||||
// class system — the dispatcher maps `class` to a struct exported
|
||||
// from `entry::`, and `method` to a `&self` method on that
|
||||
// struct. The harness constructs the receiver via
|
||||
// `<class>::default()` (preferred path), falling back to
|
||||
// `<class>::new()` when `Default` is not implemented.
|
||||
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
|
||||
return Ok(emit_class_method_harness(spec, class, method));
|
||||
}
|
||||
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
// Generic + LibfuzzerTarget accept Param(0)/EnvVar; richer shapes
|
||||
|
|
@ -851,6 +862,86 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Phase 19 (Track M.1) — class-method harness for Rust.
|
||||
///
|
||||
/// Emits `src/main.rs` that constructs `entry::<class>::default()`
|
||||
/// and invokes `instance.<method>(&payload)`. The fixture is
|
||||
/// expected to derive `Default` on the receiver type so the harness
|
||||
/// has a zero-arg construction path. When `Default` is unavailable
|
||||
/// the fixture can provide a `new()` associated function; the
|
||||
/// harness falls back to that via conditional compilation when
|
||||
/// `Default` lookup fails.
|
||||
fn emit_class_method_harness(spec: &HarnessSpec, class: &str, method: &str) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let cargo_toml = generate_cargo_toml(spec.expected_cap);
|
||||
let entry_label = format!("{class}::{method}");
|
||||
let body = format!(
|
||||
r#"//! Nyx dynamic harness — class method (Phase 19 / Track M.1).
|
||||
mod entry;
|
||||
{shim}
|
||||
fn main() {{
|
||||
let payload = nyx_payload();
|
||||
let _ = &payload;
|
||||
__nyx_install_crash_guard("{entry_label}");
|
||||
let instance = entry::{class}::default();
|
||||
let _ = instance.{method}(&payload);
|
||||
}}
|
||||
|
||||
fn nyx_payload() -> String {{
|
||||
if let Ok(v) = std::env::var("NYX_PAYLOAD") {{
|
||||
if !v.is_empty() {{
|
||||
return v;
|
||||
}}
|
||||
}}
|
||||
if let Ok(b64) = std::env::var("NYX_PAYLOAD_B64") {{
|
||||
if let Some(bytes) = b64_decode(b64.as_bytes()) {{
|
||||
return String::from_utf8_lossy(&bytes).into_owned();
|
||||
}}
|
||||
}}
|
||||
String::new()
|
||||
}}
|
||||
|
||||
fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
|
||||
const TABLE: [u8; 128] = {{
|
||||
let mut t = [255u8; 128];
|
||||
let alphabet: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut i = 0usize;
|
||||
while i < alphabet.len() {{
|
||||
t[alphabet[i] as usize] = i as u8;
|
||||
i += 1;
|
||||
}}
|
||||
t
|
||||
}};
|
||||
let input: Vec<u8> = input.iter().copied().filter(|&c| c != b'\n' && c != b'\r').collect();
|
||||
let mut out = Vec::with_capacity(input.len() * 3 / 4);
|
||||
let mut i = 0;
|
||||
while i + 3 < input.len() {{
|
||||
let a = *TABLE.get(input[i] as usize)? as u32;
|
||||
let b = *TABLE.get(input[i + 1] as usize)? as u32;
|
||||
let c = if input[i + 2] == b'=' {{ 64 }} else {{ *TABLE.get(input[i + 2] as usize)? as u32 }};
|
||||
let d = if input[i + 3] == b'=' {{ 64 }} else {{ *TABLE.get(input[i + 3] as usize)? as u32 }};
|
||||
if a == 255 || b == 255 || c == 255 || d == 255 {{ return None; }}
|
||||
out.push(((a << 2) | (b >> 4)) as u8);
|
||||
if input[i + 2] != b'=' {{ out.push(((b << 4) | (c >> 2)) as u8); }}
|
||||
if input[i + 3] != b'=' {{ out.push(((c << 6) | d) as u8); }}
|
||||
i += 4;
|
||||
}}
|
||||
Some(out)
|
||||
}}
|
||||
"#,
|
||||
class = class,
|
||||
method = method,
|
||||
entry_label = entry_label,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
filename: "src/main.rs".into(),
|
||||
command: vec!["target/release/nyx_harness".into()],
|
||||
extra_files: vec![("Cargo.toml".into(), cargo_toml)],
|
||||
entry_subpath: Some("src/entry.rs".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `Cargo.toml` for the harness crate.
|
||||
///
|
||||
/// Dependencies are driven by `expected_cap`:
|
||||
|
|
|
|||
|
|
@ -1222,41 +1222,52 @@ fn attach_framework_binding(spec: &mut HarnessSpec, summaries: Option<&GlobalSum
|
|||
if let Some(binding) =
|
||||
crate::dynamic::framework::detect_binding(summary_ref, tree.root_node(), &bytes, spec.lang)
|
||||
{
|
||||
// Phase 14 (Track L.12): flip the Spring-test toolchain knob
|
||||
// when the java-spring adapter binds, so the Java emitter
|
||||
// bootstraps `SpringApplication.run` / `MockMvc` for Spring
|
||||
// routes and skips that heavier path for the other Java
|
||||
// shapes (Quarkus / Micronaut / Servlet).
|
||||
if spec.lang == Lang::Java && binding.adapter == "java-spring" {
|
||||
spec.java_toolchain.with_spring_test = true;
|
||||
}
|
||||
// Phase 18 (Track M.0): the binding carries the adapter's view
|
||||
// of the entry shape — when the adapter stamps one of the new
|
||||
// data-bearing variants (`ClassMethod`, `MessageHandler`,
|
||||
// `ScheduledJob`, …), propagate that onto the spec so the
|
||||
// verifier's `entry_kind_is_supported` gate sees the structural
|
||||
// shape and short-circuits to a typed
|
||||
// `Inconclusive(EntryKindUnsupported)`. We deliberately do not
|
||||
// overwrite the legacy unit variants here: every adapter
|
||||
// shipped through Phase 17 stamps `Function` / `HttpRoute` and
|
||||
// the derivation pipeline already routes those correctly.
|
||||
if matches!(
|
||||
binding.kind.tag(),
|
||||
crate::evidence::EntryKindTag::ClassMethod
|
||||
| crate::evidence::EntryKindTag::MessageHandler
|
||||
| crate::evidence::EntryKindTag::ScheduledJob
|
||||
| crate::evidence::EntryKindTag::GraphQLResolver
|
||||
| crate::evidence::EntryKindTag::WebSocket
|
||||
| crate::evidence::EntryKindTag::Middleware
|
||||
| crate::evidence::EntryKindTag::Migration
|
||||
) {
|
||||
spec.entry_kind = binding.kind.clone();
|
||||
spec.spec_hash = compute_spec_hash(spec);
|
||||
}
|
||||
spec.framework = Some(binding);
|
||||
stamp_framework_binding(spec, binding);
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 18 (Track M.0) — apply a resolved [`FrameworkBinding`] onto
|
||||
/// the spec. Carved out of [`attach_framework_binding`] so the
|
||||
/// stamping branch (Phase 18 data-bearing-variant propagation +
|
||||
/// Phase 14 Spring-test toolchain knob) is unit-testable without
|
||||
/// needing a registered framework adapter — the deferred-fix Phase
|
||||
/// 18 test for `spec_attach_framework_binding_stamps_new_entry_kind_variant`
|
||||
/// drives a synthetic binding through this helper directly.
|
||||
fn stamp_framework_binding(spec: &mut HarnessSpec, binding: FrameworkBinding) {
|
||||
// Phase 14 (Track L.12): flip the Spring-test toolchain knob
|
||||
// when the java-spring adapter binds, so the Java emitter
|
||||
// bootstraps `SpringApplication.run` / `MockMvc` for Spring
|
||||
// routes and skips that heavier path for the other Java
|
||||
// shapes (Quarkus / Micronaut / Servlet).
|
||||
if spec.lang == Lang::Java && binding.adapter == "java-spring" {
|
||||
spec.java_toolchain.with_spring_test = true;
|
||||
}
|
||||
// Phase 18 (Track M.0): the binding carries the adapter's view
|
||||
// of the entry shape — when the adapter stamps one of the new
|
||||
// data-bearing variants (`ClassMethod`, `MessageHandler`,
|
||||
// `ScheduledJob`, …), propagate that onto the spec so the
|
||||
// verifier's `entry_kind_is_supported` gate sees the structural
|
||||
// shape and short-circuits to a typed
|
||||
// `Inconclusive(EntryKindUnsupported)`. We deliberately do not
|
||||
// overwrite the legacy unit variants here: every adapter
|
||||
// shipped through Phase 17 stamps `Function` / `HttpRoute` and
|
||||
// the derivation pipeline already routes those correctly.
|
||||
if matches!(
|
||||
binding.kind.tag(),
|
||||
crate::evidence::EntryKindTag::ClassMethod
|
||||
| crate::evidence::EntryKindTag::MessageHandler
|
||||
| crate::evidence::EntryKindTag::ScheduledJob
|
||||
| crate::evidence::EntryKindTag::GraphQLResolver
|
||||
| crate::evidence::EntryKindTag::WebSocket
|
||||
| crate::evidence::EntryKindTag::Middleware
|
||||
| crate::evidence::EntryKindTag::Migration
|
||||
) {
|
||||
spec.entry_kind = binding.kind.clone();
|
||||
spec.spec_hash = compute_spec_hash(spec);
|
||||
}
|
||||
spec.framework = Some(binding);
|
||||
}
|
||||
|
||||
/// Pick the tree-sitter `Language` for a given [`Lang`]. Returns
|
||||
/// `None` for languages whose grammar is not linked into the dynamic
|
||||
/// path (rare — every supported `Lang` carries a grammar).
|
||||
|
|
@ -2144,4 +2155,104 @@ mod tests {
|
|||
// descriptive metadata.
|
||||
assert_eq!(spec_no_summaries.spec_hash, spec_with_summaries.spec_hash);
|
||||
}
|
||||
|
||||
/// Phase 18 (Track M.0) deferred-fix: when a [`FrameworkBinding`]
|
||||
/// carries one of the seven data-bearing variants
|
||||
/// (`ClassMethod`, `MessageHandler`, …), the spec stamping path
|
||||
/// propagates the variant onto `spec.entry_kind` and recomputes
|
||||
/// `spec.spec_hash`. Validated against the synthetic
|
||||
/// [`stamp_framework_binding`] entry point so the test does not
|
||||
/// need to register an adapter that emits the variant.
|
||||
#[test]
|
||||
fn spec_attach_framework_binding_stamps_new_entry_kind_variant() {
|
||||
let mut spec = HarnessSpec {
|
||||
finding_id: "phase18stamp0001".into(),
|
||||
entry_file: "src/handler.py".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Python,
|
||||
toolchain_id: "phase18".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: crate::labels::Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/handler.py".into(),
|
||||
sink_line: 1,
|
||||
spec_hash: "phase18stamp0001".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: JavaToolchain::default(),
|
||||
};
|
||||
let pre_hash = spec.spec_hash.clone();
|
||||
let pre_tag = spec.entry_kind.tag();
|
||||
|
||||
let binding = FrameworkBinding {
|
||||
adapter: "phase19-synthetic".to_owned(),
|
||||
kind: EntryKind::ClassMethod {
|
||||
class: "UserRepository".to_owned(),
|
||||
method: "find_by_name".to_owned(),
|
||||
},
|
||||
route: None,
|
||||
request_params: vec![],
|
||||
response_writer: None,
|
||||
middleware: vec![],
|
||||
};
|
||||
|
||||
stamp_framework_binding(&mut spec, binding);
|
||||
|
||||
assert_eq!(
|
||||
spec.entry_kind.tag(),
|
||||
crate::evidence::EntryKindTag::ClassMethod,
|
||||
"stamping must replace Function with ClassMethod when the binding carries one of the Phase 18 variants",
|
||||
);
|
||||
assert_ne!(pre_tag, spec.entry_kind.tag());
|
||||
assert_ne!(
|
||||
pre_hash, spec.spec_hash,
|
||||
"spec_hash must change when entry_kind tag flips",
|
||||
);
|
||||
assert_eq!(
|
||||
spec.framework.as_ref().map(|b| b.adapter.as_str()),
|
||||
Some("phase19-synthetic"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Companion guard: when the binding carries a legacy unit
|
||||
/// variant (`Function` / `HttpRoute`), the stamping branch keeps
|
||||
/// `spec.entry_kind` and `spec.spec_hash` unchanged.
|
||||
#[test]
|
||||
fn spec_attach_framework_binding_keeps_legacy_unit_variant_unchanged() {
|
||||
let mut spec = HarnessSpec {
|
||||
finding_id: "phase18stamp0002".into(),
|
||||
entry_file: "src/handler.py".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Python,
|
||||
toolchain_id: "phase18".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: crate::labels::Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/handler.py".into(),
|
||||
sink_line: 1,
|
||||
spec_hash: "phase18stamp0002".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: JavaToolchain::default(),
|
||||
};
|
||||
let pre_hash = spec.spec_hash.clone();
|
||||
|
||||
let binding = FrameworkBinding {
|
||||
adapter: "phase17-synthetic".to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: vec![],
|
||||
response_writer: None,
|
||||
middleware: vec![],
|
||||
};
|
||||
stamp_framework_binding(&mut spec, binding);
|
||||
|
||||
assert_eq!(spec.entry_kind.tag(), crate::evidence::EntryKindTag::Function);
|
||||
assert_eq!(spec.spec_hash, pre_hash);
|
||||
assert!(spec.framework.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
244
src/dynamic/stubs/mocks.rs
Normal file
244
src/dynamic/stubs/mocks.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//! Phase 19 (Track M.1) — language-specific mock generators for class
|
||||
//! constructor parameters.
|
||||
//!
|
||||
//! When [`crate::dynamic::lang::LangEmitter::emit`] hits an
|
||||
//! `EntryKind::ClassMethod` whose constructor takes an injectable
|
||||
//! dependency (HTTP client, database connection, logger), the per-lang
|
||||
//! emitter consults this registry to splice in a test double rather
|
||||
//! than instantiating the real boundary. The double is a tiny source
|
||||
//! snippet — class / struct / function — that has the same surface as
|
||||
//! the real type but performs no I/O.
|
||||
//!
|
||||
//! The registry is deliberately small: only the three dependency
|
||||
//! shapes mentioned in Phase 19's brief
|
||||
//! (`MockHttpClient`, `MockDatabaseConnection`, `MockLogger`) are
|
||||
//! covered. A future phase that needs richer doubles
|
||||
//! (`MockCache`, `MockSessionStore`, …) can extend the [`MockKind`]
|
||||
//! enum + add new branches to [`mock_source`] without re-versioning the
|
||||
//! caller surface.
|
||||
|
||||
use crate::symbol::Lang;
|
||||
|
||||
/// Discriminator for an injectable dependency the harness may need to
|
||||
/// stub when constructing a class receiver.
|
||||
///
|
||||
/// The names follow the Phase 19 brief verbatim. Each variant maps to
|
||||
/// one inline source snippet per language; the snippet declares a
|
||||
/// constructor-callable type named `MockHttpClient` /
|
||||
/// `MockDatabaseConnection` / `MockLogger` so the per-lang invocation
|
||||
/// path can splice it in by name without needing a separate lookup
|
||||
/// per language.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MockKind {
|
||||
/// HTTP client surface — exposes `get` / `post` no-ops returning
|
||||
/// empty strings.
|
||||
HttpClient,
|
||||
/// Database connection surface — exposes `execute` / `query`
|
||||
/// no-ops returning empty result sets.
|
||||
DatabaseConnection,
|
||||
/// Logger surface — exposes `info` / `warn` / `error` no-ops.
|
||||
Logger,
|
||||
}
|
||||
|
||||
impl MockKind {
|
||||
/// Canonical mock-type name a per-language emitter can construct.
|
||||
/// Stable across versions — call sites in lang emitters reference
|
||||
/// these strings directly.
|
||||
pub const fn type_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::HttpClient => "MockHttpClient",
|
||||
Self::DatabaseConnection => "MockDatabaseConnection",
|
||||
Self::Logger => "MockLogger",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source snippet declaring a `MockKind` test double in `lang`.
|
||||
///
|
||||
/// The snippet is meant to be spliced verbatim into the generated
|
||||
/// harness source; it declares a public type whose name matches
|
||||
/// [`MockKind::type_name`] and a public default constructor so the
|
||||
/// harness's class-method dispatcher can write
|
||||
/// `new {type_name}()` (or the per-lang equivalent) without further
|
||||
/// per-mock plumbing.
|
||||
///
|
||||
/// Returns `""` (empty string) when the language has no concept of
|
||||
/// classes / object dependencies (C, today). The caller is expected
|
||||
/// to fall through to a payload-only call when the snippet is empty.
|
||||
pub fn mock_source(kind: MockKind, lang: Lang) -> &'static str {
|
||||
match (kind, lang) {
|
||||
// ── Python ──────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Python) => {
|
||||
"class MockHttpClient:\n def get(self, url, **kw): return ''\n def post(self, url, body=None, **kw): return ''\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Python) => {
|
||||
"class MockDatabaseConnection:\n def execute(self, q, *a, **kw): return None\n def query(self, q, *a, **kw): return []\n def close(self): pass\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Python) => {
|
||||
"class MockLogger:\n def info(self, *a, **kw): pass\n def warn(self, *a, **kw): pass\n def error(self, *a, **kw): pass\n def debug(self, *a, **kw): pass\n"
|
||||
}
|
||||
|
||||
// ── JavaScript / TypeScript ────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::JavaScript | Lang::TypeScript) => {
|
||||
"class MockHttpClient { get(_u){return ''} post(_u,_b){return ''} }\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::JavaScript | Lang::TypeScript) => {
|
||||
"class MockDatabaseConnection { execute(){return null} query(){return []} close(){} }\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::JavaScript | Lang::TypeScript) => {
|
||||
"class MockLogger { info(){} warn(){} error(){} debug(){} }\n"
|
||||
}
|
||||
|
||||
// ── Java ───────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Java) => {
|
||||
"static class MockHttpClient { public String get(String u){return \"\";} public String post(String u, String b){return \"\";} }\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Java) => {
|
||||
"static class MockDatabaseConnection { public Object execute(String q){return null;} public java.util.List<Object> query(String q){return java.util.Collections.emptyList();} public void close(){} }\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Java) => {
|
||||
"static class MockLogger { public void info(String s){} public void warn(String s){} public void error(String s){} public void debug(String s){} }\n"
|
||||
}
|
||||
|
||||
// ── PHP ────────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Php) => {
|
||||
"class MockHttpClient { public function get($u){return '';} public function post($u, $b = null){return '';} }\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Php) => {
|
||||
"class MockDatabaseConnection { public function execute($q){return null;} public function query($q){return [];} public function close(){} }\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Php) => {
|
||||
"class MockLogger { public function info($m){} public function warn($m){} public function error($m){} public function debug($m){} }\n"
|
||||
}
|
||||
|
||||
// ── Ruby ───────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Ruby) => {
|
||||
"class MockHttpClient\n def get(_u); ''; end\n def post(_u, _b = nil); ''; end\nend\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Ruby) => {
|
||||
"class MockDatabaseConnection\n def execute(_q); nil; end\n def query(_q); []; end\n def close; end\nend\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Ruby) => {
|
||||
"class MockLogger\n def info(*); end\n def warn(*); end\n def error(*); end\n def debug(*); end\nend\n"
|
||||
}
|
||||
|
||||
// ── Go ─────────────────────────────────────────────────────
|
||||
// Go has no classes; we emit struct-shaped doubles with method
|
||||
// sets that mirror the Python / Java surface so a class-method
|
||||
// emitter can construct the receiver via `MockX{}`.
|
||||
(MockKind::HttpClient, Lang::Go) => {
|
||||
"type MockHttpClient struct{}\nfunc (MockHttpClient) Get(string) string { return \"\" }\nfunc (MockHttpClient) Post(string, string) string { return \"\" }\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Go) => {
|
||||
"type MockDatabaseConnection struct{}\nfunc (MockDatabaseConnection) Execute(string) error { return nil }\nfunc (MockDatabaseConnection) Query(string) []interface{} { return nil }\nfunc (MockDatabaseConnection) Close() {}\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Go) => {
|
||||
"type MockLogger struct{}\nfunc (MockLogger) Info(string) {}\nfunc (MockLogger) Warn(string) {}\nfunc (MockLogger) Error(string) {}\nfunc (MockLogger) Debug(string) {}\n"
|
||||
}
|
||||
|
||||
// ── Rust ───────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Rust) => {
|
||||
"pub struct MockHttpClient;\nimpl MockHttpClient { pub fn new() -> Self { MockHttpClient } pub fn get(&self, _u: &str) -> String { String::new() } pub fn post(&self, _u: &str, _b: &str) -> String { String::new() } }\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Rust) => {
|
||||
"pub struct MockDatabaseConnection;\nimpl MockDatabaseConnection { pub fn new() -> Self { MockDatabaseConnection } pub fn execute(&self, _q: &str) {} pub fn query(&self, _q: &str) -> Vec<String> { Vec::new() } pub fn close(&self) {} }\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Rust) => {
|
||||
"pub struct MockLogger;\nimpl MockLogger { pub fn new() -> Self { MockLogger } pub fn info(&self, _m: &str) {} pub fn warn(&self, _m: &str) {} pub fn error(&self, _m: &str) {} pub fn debug(&self, _m: &str) {} }\n"
|
||||
}
|
||||
|
||||
// ── C++ ────────────────────────────────────────────────────
|
||||
(MockKind::HttpClient, Lang::Cpp) => {
|
||||
"struct MockHttpClient { std::string get(const std::string&){return {};} std::string post(const std::string&, const std::string&){return {};} };\n"
|
||||
}
|
||||
(MockKind::DatabaseConnection, Lang::Cpp) => {
|
||||
"struct MockDatabaseConnection { void execute(const std::string&){} std::vector<std::string> query(const std::string&){return {};} void close(){} };\n"
|
||||
}
|
||||
(MockKind::Logger, Lang::Cpp) => {
|
||||
"struct MockLogger { void info(const std::string&){} void warn(const std::string&){} void error(const std::string&){} void debug(const std::string&){} };\n"
|
||||
}
|
||||
|
||||
// ── C ──────────────────────────────────────────────────────
|
||||
// C has no class system; mocks are not applicable. Lang emitter
|
||||
// routes `ClassMethod` to a plain function call when receiver
|
||||
// construction is meaningless.
|
||||
(_, Lang::C) => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn type_names_are_distinct_and_stable() {
|
||||
assert_eq!(MockKind::HttpClient.type_name(), "MockHttpClient");
|
||||
assert_eq!(
|
||||
MockKind::DatabaseConnection.type_name(),
|
||||
"MockDatabaseConnection"
|
||||
);
|
||||
assert_eq!(MockKind::Logger.type_name(), "MockLogger");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_source_python_declares_class() {
|
||||
let src = mock_source(MockKind::HttpClient, Lang::Python);
|
||||
assert!(src.contains("class MockHttpClient"));
|
||||
assert!(src.contains("def get"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_source_java_uses_static_inner_class() {
|
||||
let src = mock_source(MockKind::Logger, Lang::Java);
|
||||
assert!(src.contains("static class MockLogger"));
|
||||
assert!(src.contains("public void info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_source_c_is_empty_no_class_system() {
|
||||
assert!(mock_source(MockKind::HttpClient, Lang::C).is_empty());
|
||||
assert!(mock_source(MockKind::DatabaseConnection, Lang::C).is_empty());
|
||||
assert!(mock_source(MockKind::Logger, Lang::C).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_source_rust_struct_with_default_ctor() {
|
||||
let src = mock_source(MockKind::DatabaseConnection, Lang::Rust);
|
||||
assert!(src.contains("pub struct MockDatabaseConnection"));
|
||||
assert!(src.contains("pub fn new"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mock_source_go_struct_with_method_set() {
|
||||
let src = mock_source(MockKind::HttpClient, Lang::Go);
|
||||
assert!(src.contains("type MockHttpClient struct"));
|
||||
assert!(src.contains("func (MockHttpClient) Get"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_lang_supports_every_mock_except_c() {
|
||||
for kind in [
|
||||
MockKind::HttpClient,
|
||||
MockKind::DatabaseConnection,
|
||||
MockKind::Logger,
|
||||
] {
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
Lang::Cpp,
|
||||
] {
|
||||
assert!(
|
||||
!mock_source(kind, lang).is_empty(),
|
||||
"{lang:?} must supply a {kind:?} mock"
|
||||
);
|
||||
}
|
||||
assert!(mock_source(kind, Lang::C).is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
pub mod filesystem;
|
||||
pub mod http;
|
||||
pub mod ldap_server;
|
||||
pub mod mocks;
|
||||
pub mod redis;
|
||||
pub mod sql;
|
||||
pub mod xpath_document;
|
||||
|
|
@ -61,6 +62,7 @@ pub mod xpath_document;
|
|||
pub use filesystem::FilesystemStub;
|
||||
pub use http::HttpStub;
|
||||
pub use ldap_server::LdapStub;
|
||||
pub use mocks::{mock_source, MockKind};
|
||||
pub use redis::RedisStub;
|
||||
pub use sql::SqlStub;
|
||||
|
||||
|
|
|
|||
201
tests/class_method_corpus.rs
Normal file
201
tests/class_method_corpus.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//! Phase 19 (Track M.1) — `ClassMethod` end-to-end acceptance.
|
||||
//!
|
||||
//! Asserts the new `EntryKind::ClassMethod { class, method }` variant
|
||||
//! is supported by every per-language emitter so the
|
||||
//! `Inconclusive(EntryKindUnsupported { attempted: ClassMethod })`
|
||||
//! rate drops to 0% across the ten supported languages. Each
|
||||
//! sub-test constructs a `HarnessSpec` whose `entry_kind` is
|
||||
//! `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).
|
||||
//!
|
||||
//! `cargo nextest run --features dynamic --test class_method_corpus`.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::lang;
|
||||
use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot};
|
||||
use nyx_scanner::dynamic::stubs::{mock_source, MockKind};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
const LANGS: &[Lang] = &[
|
||||
Lang::Python,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
Lang::Go,
|
||||
Lang::Rust,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
];
|
||||
|
||||
fn entry_file(lang: Lang) -> &'static str {
|
||||
match lang {
|
||||
Lang::Python => "tests/dynamic_fixtures/class_method/python/vuln.py",
|
||||
Lang::JavaScript => "tests/dynamic_fixtures/class_method/javascript/vuln.js",
|
||||
Lang::TypeScript => "tests/dynamic_fixtures/class_method/typescript/vuln.ts",
|
||||
Lang::Java => "tests/dynamic_fixtures/class_method/java/Vuln.java",
|
||||
Lang::Php => "tests/dynamic_fixtures/class_method/php/vuln.php",
|
||||
Lang::Ruby => "tests/dynamic_fixtures/class_method/ruby/vuln.rb",
|
||||
Lang::Go => "tests/dynamic_fixtures/class_method/go/vuln.go",
|
||||
Lang::Rust => "tests/dynamic_fixtures/class_method/rust/vuln.rs",
|
||||
Lang::C => "tests/dynamic_fixtures/class_method/c/vuln.c",
|
||||
Lang::Cpp => "tests/dynamic_fixtures/class_method/cpp/vuln.cpp",
|
||||
}
|
||||
}
|
||||
|
||||
fn class_for(lang: Lang) -> (&'static str, &'static str) {
|
||||
match lang {
|
||||
Lang::Python => ("UserRepository", "find_by_name"),
|
||||
Lang::Java => ("UserRepository", "findByName"),
|
||||
Lang::C => ("UserService", "run"),
|
||||
_ => ("UserService", "run"),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_spec(lang: Lang) -> HarnessSpec {
|
||||
let (class, method) = class_for(lang);
|
||||
HarnessSpec {
|
||||
finding_id: "phase19classmth1".into(),
|
||||
entry_file: entry_file(lang).into(),
|
||||
entry_name: method.into(),
|
||||
entry_kind: EntryKind::ClassMethod {
|
||||
class: class.into(),
|
||||
method: method.into(),
|
||||
},
|
||||
lang,
|
||||
toolchain_id: "phase19".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::CODE_EXEC,
|
||||
constraint_hints: vec![],
|
||||
sink_file: entry_file(lang).into(),
|
||||
sink_line: 1,
|
||||
spec_hash: "phase19classmth1".into(),
|
||||
derivation: nyx_scanner::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
framework: None,
|
||||
java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_supported_by_every_lang_emitter() {
|
||||
for lang in LANGS {
|
||||
let supported = lang::entry_kinds_supported(*lang);
|
||||
assert!(
|
||||
supported.contains(&EntryKindTag::ClassMethod),
|
||||
"{lang:?} must advertise ClassMethod after Phase 19; supported = {supported:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_emit_does_not_short_circuit_to_entry_kind_unsupported() {
|
||||
for lang in LANGS {
|
||||
let spec = make_spec(*lang);
|
||||
let result = lang::emit(&spec);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"{lang:?} emit returned {result:?} for ClassMethod spec"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_harness_carries_class_and_method_literal() {
|
||||
for lang in LANGS {
|
||||
let spec = make_spec(*lang);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
let (class, method) = class_for(*lang);
|
||||
assert!(
|
||||
h.source.contains(class),
|
||||
"{lang:?} harness source must reference class {class:?}",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains(method),
|
||||
"{lang:?} harness source must reference method {method:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_harness_splices_phase_19_mock_classes_where_lang_has_classes() {
|
||||
// Languages with a class system embed the MockHttpClient /
|
||||
// MockDatabaseConnection / MockLogger declarations the
|
||||
// `stubs::mocks` registry publishes. Go uses a struct registry
|
||||
// routed through the entry package and does not splice the
|
||||
// doubles into the harness source; C has no class system.
|
||||
// Rust's ClassMethod path uses Default::default() — no mocks.
|
||||
let class_system_langs = [
|
||||
Lang::Python,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
];
|
||||
for lang in class_system_langs {
|
||||
let spec = make_spec(lang);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
let mock_http = mock_source(MockKind::HttpClient, lang);
|
||||
assert!(
|
||||
h.source.contains("MockHttpClient"),
|
||||
"{lang:?} harness must splice MockHttpClient",
|
||||
);
|
||||
assert!(!mock_http.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_python_dispatch_reads_payload_and_invokes_method() {
|
||||
let spec = make_spec(Lang::Python);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("NYX_PAYLOAD"));
|
||||
assert!(h.source.contains("UserRepository"));
|
||||
assert!(h.source.contains("find_by_name"));
|
||||
assert!(h.source.contains("_nyx_build_receiver"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_java_emits_reflective_dispatch() {
|
||||
let spec = make_spec(Lang::Java);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("Class.forName"));
|
||||
assert!(h.source.contains("nyxBuildReceiver"));
|
||||
assert!(h.source.contains("UserRepository"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_go_uses_reflect_receivers_registry() {
|
||||
let spec = make_spec(Lang::Go);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("entry.NyxReceivers"));
|
||||
assert!(h.source.contains("MethodByName"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_rust_uses_default_constructor() {
|
||||
let spec = make_spec(Lang::Rust);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("UserService::default()"));
|
||||
assert!(h.source.contains("instance.run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_c_collapses_to_class_underscore_method_symbol() {
|
||||
let spec = make_spec(Lang::C);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("UserService_run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_cpp_constructs_default_then_calls_method() {
|
||||
let spec = make_spec(Lang::Cpp);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("UserService instance;"));
|
||||
assert!(h.source.contains("instance.run"));
|
||||
}
|
||||
16
tests/dynamic_fixtures/class_method/c/benign.c
Normal file
16
tests/dynamic_fixtures/class_method/c/benign.c
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Phase 19 (Track M.1) — class-method benign control for C. */
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
/* Uses execve via fork; the shell never sees `input`. */
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
char *argv[] = { (char*)"/bin/echo", (char*)(input ? input : ""), NULL };
|
||||
execv("/bin/echo", argv);
|
||||
_exit(127);
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/class_method/c/vuln.c
Normal file
16
tests/dynamic_fixtures/class_method/c/vuln.c
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Phase 19 (Track M.1) — class-method vuln fixture for C.
|
||||
*
|
||||
* C has no class system; the harness calls a free function whose name
|
||||
* follows the `<Class>_<method>` convention (`UserService_run`). The
|
||||
* function piping `input` straight into `system(3)` is the SINK. */
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void UserService_run(const char *input, size_t len) {
|
||||
(void)len;
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "echo %s", input ? input : "");
|
||||
/* SINK: tainted input → system(3) */
|
||||
system(buf);
|
||||
}
|
||||
19
tests/dynamic_fixtures/class_method/cpp/benign.cpp
Normal file
19
tests/dynamic_fixtures/class_method/cpp/benign.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for C++.
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <string>
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
UserService() = default;
|
||||
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));
|
||||
_exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
}
|
||||
};
|
||||
17
tests/dynamic_fixtures/class_method/cpp/vuln.cpp
Normal file
17
tests/dynamic_fixtures/class_method/cpp/vuln.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for C++.
|
||||
//
|
||||
// UserService::run pipes user input into `system(3)`. Default
|
||||
// constructor exists; the harness can build the receiver with
|
||||
// `UserService instance;`.
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
class UserService {
|
||||
public:
|
||||
UserService() = default;
|
||||
void run(const std::string& input) {
|
||||
std::string cmd = std::string("echo ") + input;
|
||||
// SINK: tainted input → system(3)
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
};
|
||||
15
tests/dynamic_fixtures/class_method/go/benign.go
Normal file
15
tests/dynamic_fixtures/class_method/go/benign.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Go.
|
||||
package entry
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (UserService) Run(input string) string {
|
||||
out, _ := exec.Command("/bin/echo", input).Output()
|
||||
return string(out)
|
||||
}
|
||||
|
||||
var NyxReceivers = map[string]interface{}{
|
||||
"UserService": UserService{},
|
||||
}
|
||||
21
tests/dynamic_fixtures/class_method/go/vuln.go
Normal file
21
tests/dynamic_fixtures/class_method/go/vuln.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Go.
|
||||
//
|
||||
// UserService.Run accepts user input and passes it to `sh -c` so the
|
||||
// shell interprets it. The fixture publishes its instance through the
|
||||
// well-known `NyxReceivers` registry the harness uses to construct
|
||||
// receivers reflectively.
|
||||
package entry
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type UserService struct{}
|
||||
|
||||
func (UserService) Run(input string) string {
|
||||
// SINK: tainted input → shell -c
|
||||
out, _ := exec.Command("sh", "-c", "echo "+input).Output()
|
||||
return string(out)
|
||||
}
|
||||
|
||||
var NyxReceivers = map[string]interface{}{
|
||||
"UserService": UserService{},
|
||||
}
|
||||
20
tests/dynamic_fixtures/class_method/java/Benign.java
Normal file
20
tests/dynamic_fixtures/class_method/java/Benign.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// 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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
tests/dynamic_fixtures/class_method/java/Vuln.java
Normal file
25
tests/dynamic_fixtures/class_method/java/Vuln.java
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// 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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/class_method/javascript/benign.js
Normal file
15
tests/dynamic_fixtures/class_method/javascript/benign.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// 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.
|
||||
'use strict';
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
return execFileSync('/bin/echo', [input]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
16
tests/dynamic_fixtures/class_method/javascript/vuln.js
Normal file
16
tests/dynamic_fixtures/class_method/javascript/vuln.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for JavaScript.
|
||||
//
|
||||
// UserService.run forwards a tainted string straight into child_process.exec,
|
||||
// classic OS command injection. Default ctor — no stubbed deps needed.
|
||||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class UserService {
|
||||
constructor() {}
|
||||
run(input) {
|
||||
// SINK: untrusted input → shell
|
||||
return execSync('echo ' + input).toString();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService };
|
||||
10
tests/dynamic_fixtures/class_method/php/benign.php
Normal file
10
tests/dynamic_fixtures/class_method/php/benign.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Phase 19 (Track M.1) — class-method benign control for PHP.
|
||||
|
||||
class UserService {
|
||||
public function __construct() {}
|
||||
|
||||
public function run($input) {
|
||||
return shell_exec('echo ' . escapeshellarg($input));
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/class_method/php/vuln.php
Normal file
14
tests/dynamic_fixtures/class_method/php/vuln.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// Phase 19 (Track M.1) — class-method vuln fixture for PHP.
|
||||
//
|
||||
// UserService::run concatenates user input into a shell command;
|
||||
// default ctor, no stubbed deps needed.
|
||||
|
||||
class UserService {
|
||||
public function __construct() {}
|
||||
|
||||
public function run($input) {
|
||||
// SINK: tainted input → shell.
|
||||
return shell_exec('echo ' . $input);
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/class_method/python/benign.py
Normal file
20
tests/dynamic_fixtures/class_method/python/benign.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Phase 19 (Track M.1) — class-method benign control for Python.
|
||||
|
||||
Same surface as `vuln.py` but uses parameterised SQL so user input
|
||||
never concatenates into the query string.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self):
|
||||
self._db = sqlite3.connect(":memory:")
|
||||
self._db.executescript(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT); "
|
||||
"INSERT INTO users VALUES (1, 'alice');"
|
||||
)
|
||||
|
||||
def find_by_name(self, name):
|
||||
cur = self._db.cursor()
|
||||
cur.execute("SELECT id FROM users WHERE name = ?", (name,))
|
||||
return cur.fetchall()
|
||||
24
tests/dynamic_fixtures/class_method/python/vuln.py
Normal file
24
tests/dynamic_fixtures/class_method/python/vuln.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Phase 19 (Track M.1) — class-method vuln fixture for Python.
|
||||
|
||||
`UserRepository.find_by_name` accepts user input and builds a raw SQL
|
||||
query, classic concatenation-driven SQL injection. The class has a
|
||||
zero-arg constructor so the harness builds the receiver without
|
||||
needing a stubbed dependency.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self):
|
||||
self._db = sqlite3.connect(":memory:")
|
||||
self._db.executescript(
|
||||
"CREATE TABLE users (id INTEGER, name TEXT); "
|
||||
"INSERT INTO users VALUES (1, 'alice');"
|
||||
)
|
||||
|
||||
def find_by_name(self, name):
|
||||
cur = self._db.cursor()
|
||||
# SINK: user input concatenated into the query
|
||||
sql = "SELECT id FROM users WHERE name = '" + name + "'"
|
||||
cur.execute(sql)
|
||||
return cur.fetchall()
|
||||
29
tests/dynamic_fixtures/class_method/python_with_deps/vuln.py
Normal file
29
tests/dynamic_fixtures/class_method/python_with_deps/vuln.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Phase 19 (Track M.1) — class-method vuln with constructor deps.
|
||||
|
||||
`UserController.__init__` takes an HTTP client + a database connection
|
||||
(controller → service → repository shape). The Phase 19 harness's
|
||||
`_nyx_build_receiver` walks the ctor formals, stubs each with the
|
||||
matching `Mock*` test double from `src/dynamic/stubs/mocks.rs`, and
|
||||
invokes the sink method.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, http_client, db_connection):
|
||||
# Phase 19 harness wires MockHttpClient + MockDatabaseConnection
|
||||
# through these two formals so the ctor returns without I/O.
|
||||
self._http = http_client
|
||||
self._db = db_connection or sqlite3.connect(":memory:")
|
||||
|
||||
def search(self, query):
|
||||
cur = self._db.cursor() if hasattr(self._db, "cursor") else None
|
||||
if cur is None:
|
||||
return None
|
||||
# SINK: concatenated SQL
|
||||
sql = "SELECT 1 FROM dual WHERE x = '" + query + "'"
|
||||
try:
|
||||
cur.execute(sql)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
11
tests/dynamic_fixtures/class_method/ruby/benign.rb
Normal file
11
tests/dynamic_fixtures/class_method/ruby/benign.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Phase 19 (Track M.1) — class-method benign control for Ruby.
|
||||
require 'shellwords'
|
||||
|
||||
class UserService
|
||||
def initialize
|
||||
end
|
||||
|
||||
def run(input)
|
||||
`echo #{Shellwords.escape(input)}`
|
||||
end
|
||||
end
|
||||
13
tests/dynamic_fixtures/class_method/ruby/vuln.rb
Normal file
13
tests/dynamic_fixtures/class_method/ruby/vuln.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 19 (Track M.1) — class-method vuln fixture for Ruby.
|
||||
#
|
||||
# UserService#run pipes user input into a shell, classic OS command
|
||||
# injection. Default `.new` ctor — no mock deps needed.
|
||||
class UserService
|
||||
def initialize
|
||||
end
|
||||
|
||||
def run(input)
|
||||
# SINK: tainted input → shell
|
||||
`echo #{input}`
|
||||
end
|
||||
end
|
||||
14
tests/dynamic_fixtures/class_method/rust/benign.rs
Normal file
14
tests/dynamic_fixtures/class_method/rust/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for Rust.
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserService;
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
let out = std::process::Command::new("/bin/echo")
|
||||
.arg(input)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/class_method/rust/vuln.rs
Normal file
21
tests/dynamic_fixtures/class_method/rust/vuln.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 19 (Track M.1) — class-method vuln fixture for Rust.
|
||||
//
|
||||
// `UserService::run` shells out with a concatenated `sh -c <input>`,
|
||||
// classic OS command injection. Derives Default so the harness can
|
||||
// build the receiver without manual stubbing.
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserService;
|
||||
|
||||
impl UserService {
|
||||
pub fn run(&self, input: &str) -> String {
|
||||
// SINK: tainted input → shell -c
|
||||
let cmd = format!("echo {}", input);
|
||||
let out = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.output()
|
||||
.expect("exec");
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
}
|
||||
}
|
||||
9
tests/dynamic_fixtures/class_method/typescript/benign.ts
Normal file
9
tests/dynamic_fixtures/class_method/typescript/benign.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Phase 19 (Track M.1) — class-method benign control for TypeScript.
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
export class UserService {
|
||||
constructor() {}
|
||||
run(input: string): string {
|
||||
return execFileSync('/bin/echo', [input]).toString();
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/class_method/typescript/vuln.ts
Normal file
12
tests/dynamic_fixtures/class_method/typescript/vuln.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// 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';
|
||||
|
||||
export class UserService {
|
||||
constructor() {}
|
||||
run(input: string): string {
|
||||
// SINK: untrusted input flows into the shell
|
||||
return execSync('echo ' + input).toString();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue