[pitboss] phase 19: Track M.1 — ClassMethod end-to-end (all langs)

This commit is contained in:
pitboss 2026-05-20 14:32:00 -05:00
parent 1b2f9cb7ca
commit b374f89577
35 changed files with 1894 additions and 41 deletions

View 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"));
}

View 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);
}
}

View 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);
}

View 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);
}
};

View 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());
}
};

View 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{},
}

View 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{},
}

View 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();
}
}
}

View 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();
}
}
}

View 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 };

View 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 };

View 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));
}
}

View 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);
}
}

View 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()

View 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()

View 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

View 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

View 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

View 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()
}
}

View 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()
}
}

View 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();
}
}

View 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();
}
}