mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
[pitboss] phase 05: M5 — JS/TS, Go, Java, PHP harness emitters
This commit is contained in:
parent
84638e7d57
commit
345b44d3cc
103 changed files with 5637 additions and 34 deletions
219
src/dynamic/lang/go.rs
Normal file
219
src/dynamic/lang/go.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
//! Go harness emitter.
|
||||
//!
|
||||
//! Generates a Go `main` package that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. Imports the entry package from `./entry/` and calls the entry function.
|
||||
//! 3. Uses `runtime.Caller`-style wrapping in fixtures for sink-reachability
|
||||
//! probes (fixtures explicitly emit `__NYX_SINK_HIT__` before the sink).
|
||||
//!
|
||||
//! Build step: `prepare_go()` in `build_sandbox.rs` runs `go build -o nyx_harness .`
|
||||
//! in the workdir. The harness command is updated to the compiled binary path.
|
||||
//!
|
||||
//! File layout in workdir:
|
||||
//! ```text
|
||||
//! main.go ← harness entry point (generated)
|
||||
//! go.mod ← module definition (generated)
|
||||
//! entry/
|
||||
//! entry.go ← entry function (copied from project; must have `package entry`)
|
||||
//! ```
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(0)` — pass payload as `string` first argument.
|
||||
//! - `PayloadSlot::EnvVar(name)` — set env var before calling entry.
|
||||
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
|
||||
//!
|
||||
//! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Emit a Go harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {}
|
||||
_ => return Err(UnsupportedReason::EntryKindUnsupported),
|
||||
}
|
||||
|
||||
let main_go = generate_main_go(spec);
|
||||
let go_mod = generate_go_mod();
|
||||
|
||||
Ok(HarnessSource {
|
||||
source: main_go,
|
||||
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()),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_main_go(spec: &HarnessSpec) -> String {
|
||||
let entry_fn = capitalize_first(&spec.entry_name);
|
||||
let (pre_call, call_expr) = build_call(spec, &entry_fn);
|
||||
|
||||
// Determine which imports are needed.
|
||||
let env_import = if matches!(&spec.payload_slot, PayloadSlot::EnvVar(_)) {
|
||||
""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let _ = env_import;
|
||||
|
||||
format!(
|
||||
r#"// Nyx dynamic harness — auto-generated, do not edit.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"nyx-harness/entry"
|
||||
)
|
||||
|
||||
func main() {{
|
||||
payload := nyxPayload()
|
||||
{pre_call} {call_expr}
|
||||
_ = fmt.Sprintf("") // suppress unused import if call_expr uses fmt directly
|
||||
_ = os.Stderr // suppress unused import
|
||||
}}
|
||||
|
||||
func nyxPayload() string {{
|
||||
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
|
||||
return v
|
||||
}}
|
||||
if b64 := os.Getenv("NYX_PAYLOAD_B64"); b64 != "" {{
|
||||
if data, err := base64.StdEncoding.DecodeString(b64); err == nil {{
|
||||
return string(data)
|
||||
}}
|
||||
}}
|
||||
return ""
|
||||
}}
|
||||
"#,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_go_mod() -> String {
|
||||
"module nyx-harness\n\ngo 1.21\n".to_owned()
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, entry_fn: &str) -> (String, String) {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => {
|
||||
let pre = String::new();
|
||||
let call = format!("entry.{entry_fn}(payload)");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
let pre = format!("\tos.Setenv({name:?}, payload)\n");
|
||||
let call = format!("entry.{entry_fn}()");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
let pre = String::new();
|
||||
let call = format!("entry.{entry_fn}(payload)");
|
||||
(pre, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Capitalize the first character of a string (Go exported names must start uppercase).
|
||||
pub fn capitalize_first(s: &str) -> String {
|
||||
let mut c = s.chars();
|
||||
match c.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "go0000000000001".into(),
|
||||
entry_file: "cmd/server/main.go".into(),
|
||||
entry_name: "handleRequest".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Go,
|
||||
toolchain_id: "go-stable".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "cmd/server/main.go".into(),
|
||||
sink_line: 20,
|
||||
spec_hash: "go0000000000001".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("nyx-harness/entry"));
|
||||
assert!(harness.source.contains("nyxPayload()"));
|
||||
assert!(harness.source.contains("entry.HandleRequest(payload)"));
|
||||
assert_eq!(harness.filename, "main.go");
|
||||
assert_eq!(harness.command, vec!["./nyx_harness"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_includes_go_mod_in_extra_files() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
let go_mod = harness.extra_files.iter().find(|(n, _)| n == "go.mod");
|
||||
assert!(go_mod.is_some(), "go.mod must be in extra_files");
|
||||
assert!(go_mod.unwrap().1.contains("module nyx-harness"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_entry_subpath_is_entry_go() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("entry/entry.go".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_env_var_slot() {
|
||||
let spec = make_spec(PayloadSlot::EnvVar("DB_USER".into()));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("os.Setenv"));
|
||||
assert!(harness.source.contains("\"DB_USER\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_gt_0_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::Param(1));
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_stdin_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::Stdin);
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capitalize_first_handles_lowercase() {
|
||||
assert_eq!(capitalize_first("handleRequest"), "HandleRequest");
|
||||
assert_eq!(capitalize_first("run"), "Run");
|
||||
assert_eq!(capitalize_first(""), "");
|
||||
assert_eq!(capitalize_first("A"), "A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_mod_has_correct_module() {
|
||||
let go_mod = generate_go_mod();
|
||||
assert!(go_mod.contains("module nyx-harness"));
|
||||
assert!(go_mod.contains("go 1.21"));
|
||||
}
|
||||
}
|
||||
191
src/dynamic/lang/java.rs
Normal file
191
src/dynamic/lang/java.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
//! Java harness emitter.
|
||||
//!
|
||||
//! Generates a Java `NyxHarness.java` that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. Calls `Entry.{entry_name}(payload)` from the co-located `Entry.java`.
|
||||
//! 3. Catches all exceptions to prevent harness crashes from masking results.
|
||||
//!
|
||||
//! Sink-reachability probe: fixtures explicitly emit `System.out.println("__NYX_SINK_HIT__")`
|
||||
//! before the actual sink call (same pattern as Rust and Go fixtures).
|
||||
//!
|
||||
//! Build step: `prepare_java()` in `build_sandbox.rs` runs `javac NyxHarness.java Entry.java`
|
||||
//! in the workdir. The compiled `.class` files land in the workdir.
|
||||
//!
|
||||
//! File layout in workdir:
|
||||
//! ```text
|
||||
//! NyxHarness.java ← harness main class (generated)
|
||||
//! Entry.java ← entry class (copied from project)
|
||||
//! NyxHarness.class ← compiled by prepare_java()
|
||||
//! Entry.class ← compiled by prepare_java()
|
||||
//! ```
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(0)` — pass payload as `String` first argument.
|
||||
//! - `PayloadSlot::EnvVar(name)` — set system property before calling entry.
|
||||
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
|
||||
//!
|
||||
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Emit a Java harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {}
|
||||
_ => return Err(UnsupportedReason::EntryKindUnsupported),
|
||||
}
|
||||
|
||||
let source = generate_harness_java(spec);
|
||||
|
||||
Ok(HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
// Use absolute workdir classpath set by runner.rs after compilation.
|
||||
// Before runner.rs updates it, '.' works for process backend when run
|
||||
// from the workdir.
|
||||
command: vec![
|
||||
"java".to_owned(),
|
||||
"-cp".to_owned(),
|
||||
".".to_owned(),
|
||||
"NyxHarness".to_owned(),
|
||||
],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some("Entry.java".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_harness_java(spec: &HarnessSpec) -> String {
|
||||
let entry_method = &spec.entry_name;
|
||||
let (pre_call, call_expr) = build_call(spec, entry_method);
|
||||
|
||||
format!(
|
||||
r#"// Nyx dynamic harness — auto-generated, do not edit.
|
||||
public class NyxHarness {{
|
||||
public static void main(String[] args) throws Exception {{
|
||||
String payload = nyxPayload();
|
||||
{pre_call} try {{
|
||||
{call_expr}
|
||||
}} catch (Exception 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 "";
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, method: &str) -> (String, String) {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => {
|
||||
let pre = String::new();
|
||||
let call = format!("Entry.{method}(payload);");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
// Use System.setProperty since env vars cannot be set post-JVM-launch
|
||||
// via standard Java APIs. Fixtures that read env vars must use
|
||||
// System.getProperty as a fallback, or read NYX_PAYLOAD_PROP_{name}.
|
||||
let pre = format!(
|
||||
" System.setProperty({name:?}, payload);\n"
|
||||
);
|
||||
let call = format!("Entry.{method}();");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
let pre = String::new();
|
||||
let call = format!("Entry.{method}(payload);");
|
||||
(pre, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "java00000000001".into(),
|
||||
entry_file: "src/main/java/App.java".into(),
|
||||
entry_name: "processInput".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Java,
|
||||
toolchain_id: "java-21".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/main/java/App.java".into(),
|
||||
sink_line: 25,
|
||||
spec_hash: "java00000000001".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("public class NyxHarness"));
|
||||
assert!(harness.source.contains("nyxPayload()"));
|
||||
assert!(harness.source.contains("Entry.processInput(payload)"));
|
||||
assert_eq!(harness.filename, "NyxHarness.java");
|
||||
assert_eq!(harness.command, vec!["java", "-cp", ".", "NyxHarness"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_entry_subpath_is_entry_java() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_env_var_slot() {
|
||||
let spec = make_spec(PayloadSlot::EnvVar("DB_PASSWORD".into()));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("System.setProperty"));
|
||||
assert!(harness.source.contains("\"DB_PASSWORD\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_gt_0_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::Param(1));
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_stdin_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::Stdin);
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_has_base64_decoder() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("Base64.getDecoder()"));
|
||||
assert!(harness.source.contains("NYX_PAYLOAD_B64"));
|
||||
}
|
||||
}
|
||||
248
src/dynamic/lang/javascript.rs
Normal file
248
src/dynamic/lang/javascript.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
//! JavaScript / TypeScript harness emitter.
|
||||
//!
|
||||
//! Generates a Node.js script that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. Requires the entry module from the workdir (`entry.js`).
|
||||
//! 3. Calls the entry function with the payload routed to the correct slot.
|
||||
//! 4. Catches all exceptions to prevent harness crashes from masking results.
|
||||
//!
|
||||
//! Sink-reachability probe: the fixture itself emits `__NYX_SINK_HIT__` before
|
||||
//! the actual sink call (same pattern as Rust fixtures). The harness is a pure
|
||||
//! runner with no line-level tracing.
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(n)` — n-th positional argument.
|
||||
//! - `PayloadSlot::EnvVar(name)` — set env var before calling.
|
||||
//! - `PayloadSlot::Stdin` — pipe payload to process.stdin.
|
||||
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
|
||||
//!
|
||||
//! Build: no compilation step. Command is `node harness.js`.
|
||||
//! Build container: `nyx-build-node:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Emit a Node.js harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::Stdin => {}
|
||||
_ => return Err(UnsupportedReason::EntryKindUnsupported),
|
||||
}
|
||||
|
||||
let source = generate_source(spec);
|
||||
let entry_filename = entry_module_filename(&spec.entry_file);
|
||||
|
||||
Ok(HarnessSource {
|
||||
source,
|
||||
filename: "harness.js".to_owned(),
|
||||
command: vec!["node".to_owned(), "harness.js".to_owned()],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some(entry_filename),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_source(spec: &HarnessSpec) -> String {
|
||||
let entry_module = entry_module_name(&spec.entry_file);
|
||||
let entry_fn = &spec.entry_name;
|
||||
let (pre_call, call_expr) = build_call(spec, &entry_module, entry_fn);
|
||||
|
||||
format!(
|
||||
r#"'use strict';
|
||||
// Nyx dynamic harness — auto-generated, do not edit.
|
||||
|
||||
// ── Payload loading ────────────────────────────────────────────────────────────
|
||||
const _nyx_payload = (() => {{
|
||||
if (process.env.NYX_PAYLOAD && process.env.NYX_PAYLOAD.length > 0) {{
|
||||
return process.env.NYX_PAYLOAD;
|
||||
}}
|
||||
if (process.env.NYX_PAYLOAD_B64 && process.env.NYX_PAYLOAD_B64.length > 0) {{
|
||||
return Buffer.from(process.env.NYX_PAYLOAD_B64, 'base64').toString('utf8');
|
||||
}}
|
||||
return '';
|
||||
}})();
|
||||
|
||||
// ── Entry module import ────────────────────────────────────────────────────────
|
||||
let _entry;
|
||||
try {{
|
||||
_entry = require('./{entry_module}');
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
|
||||
process.exit(77);
|
||||
}}
|
||||
|
||||
const payload = _nyx_payload;
|
||||
|
||||
// ── Pre-call setup ─────────────────────────────────────────────────────────────
|
||||
{pre_call}
|
||||
// ── Call entry point ──────────────────────────────────────────────────────────
|
||||
try {{
|
||||
const _result = {call_expr};
|
||||
if (_result !== undefined && _result !== null) {{
|
||||
if (_result && typeof _result.then === 'function') {{
|
||||
_result
|
||||
.then(r => {{ if (r != null) process.stdout.write(String(r) + '\n'); }})
|
||||
.catch(e => {{ process.stderr.write('NYX_EXCEPTION: ' + e.message + '\n'); }});
|
||||
}} else {{
|
||||
process.stdout.write(String(_result) + '\n');
|
||||
}}
|
||||
}}
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
|
||||
}}
|
||||
"#,
|
||||
entry_module = entry_module,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, _module: &str, func: &str) -> (String, String) {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(idx) => {
|
||||
let pre = String::new();
|
||||
let call = if *idx == 0 {
|
||||
format!("_entry.{func}(payload)")
|
||||
} else {
|
||||
let pads = (0..*idx).map(|_| "''").collect::<Vec<_>>().join(", ");
|
||||
format!("_entry.{func}({pads}, payload)")
|
||||
};
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
let pre = format!("process.env[{name:?}] = payload;\n");
|
||||
let call = format!("_entry.{func}()");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::Stdin => {
|
||||
// Synchronous stdin replacement via Buffer.
|
||||
let pre = format!(
|
||||
"const {{ Readable }} = require('stream');\n\
|
||||
process.stdin = Readable.from([Buffer.from(payload, 'utf8')]);\n"
|
||||
);
|
||||
let call = format!("_entry.{func}()");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
let pre = String::new();
|
||||
let call = format!("_entry.{func}(payload)");
|
||||
(pre, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the JS module name from an entry file path.
|
||||
///
|
||||
/// `"src/handlers/login.js"` → `"login"` (basename without extension).
|
||||
pub fn entry_module_name(entry_file: &str) -> String {
|
||||
let base = entry_file
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(entry_file)
|
||||
.rsplit('\\')
|
||||
.next()
|
||||
.unwrap_or(entry_file);
|
||||
// Strip known JS/TS extensions.
|
||||
for ext in &[".js", ".mjs", ".cjs", ".ts", ".mts"] {
|
||||
if let Some(stem) = base.strip_suffix(ext) {
|
||||
return stem.to_owned();
|
||||
}
|
||||
}
|
||||
base.to_owned()
|
||||
}
|
||||
|
||||
/// Derive the filename for `entry_subpath` from an entry file path.
|
||||
///
|
||||
/// Always returns `"entry.js"` — fixture files are copied here regardless of
|
||||
/// their original name so the harness can always `require('./entry')`.
|
||||
pub fn entry_module_filename(_entry_file: &str) -> String {
|
||||
"entry.js".to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "js000000000001".into(),
|
||||
entry_file: "src/app.js".into(),
|
||||
entry_name: "login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::JavaScript,
|
||||
toolchain_id: "node-20".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/app.js".into(),
|
||||
sink_line: 15,
|
||||
spec_hash: "js000000000001".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("NYX_PAYLOAD"));
|
||||
assert!(harness.source.contains("require"));
|
||||
assert!(harness.source.contains("login"));
|
||||
assert_eq!(harness.filename, "harness.js");
|
||||
assert_eq!(harness.command, vec!["node", "harness.js"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_index_0() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("_entry.login(payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_index_1() {
|
||||
let spec = make_spec(PayloadSlot::Param(1));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("_entry.login('', payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_env_var_slot() {
|
||||
let spec = make_spec(PayloadSlot::EnvVar("DB_HOST".into()));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("process.env[\"DB_HOST\"] = payload"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_stdin_slot() {
|
||||
let spec = make_spec(PayloadSlot::Stdin);
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("Readable"));
|
||||
assert!(harness.source.contains("process.stdin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_http_body_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::HttpBody);
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_entry_subpath_is_entry_js() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("entry.js".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_module_name_strips_extensions() {
|
||||
assert_eq!(entry_module_name("src/handlers/login.js"), "login");
|
||||
assert_eq!(entry_module_name("app.ts"), "app");
|
||||
assert_eq!(entry_module_name("handler.mjs"), "handler");
|
||||
assert_eq!(entry_module_name("no_ext"), "no_ext");
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@
|
|||
//! Each submodule implements `emit(spec) -> HarnessSource` for one language.
|
||||
//! The top-level [`emit`] function dispatches on `spec.lang`.
|
||||
|
||||
pub mod go;
|
||||
pub mod java;
|
||||
pub mod javascript;
|
||||
pub mod php;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
|
||||
|
|
@ -34,6 +38,10 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
match spec.lang {
|
||||
Lang::Python => python::emit(spec),
|
||||
Lang::Rust => rust::emit(spec),
|
||||
Lang::JavaScript | Lang::TypeScript => javascript::emit(spec),
|
||||
Lang::Go => go::emit(spec),
|
||||
Lang::Java => java::emit(spec),
|
||||
Lang::Php => php::emit(spec),
|
||||
_ => Err(UnsupportedReason::LangUnsupported),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
202
src/dynamic/lang/php.rs
Normal file
202
src/dynamic/lang/php.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
//! PHP harness emitter.
|
||||
//!
|
||||
//! Generates a PHP script that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. Includes the entry file (`entry.php`) from the workdir.
|
||||
//! 3. Calls the entry function with the payload routed to the correct slot.
|
||||
//! 4. Catches all Throwables to prevent harness crashes from masking results.
|
||||
//!
|
||||
//! Sink-reachability probe: fixtures explicitly emit `__NYX_SINK_HIT__` before
|
||||
//! the actual sink call (same pattern as Rust / JS fixtures).
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(n)` — n-th positional argument.
|
||||
//! - `PayloadSlot::EnvVar(name)` — set `$_ENV`/`putenv()` before calling.
|
||||
//! - `PayloadSlot::Stdin` — wrap `STDIN` with the payload.
|
||||
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
|
||||
//!
|
||||
//! Build: no compilation step. Command is `php harness.php`.
|
||||
//! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Emit a PHP harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::Stdin => {}
|
||||
_ => return Err(UnsupportedReason::EntryKindUnsupported),
|
||||
}
|
||||
|
||||
let source = generate_source(spec);
|
||||
|
||||
Ok(HarnessSource {
|
||||
source,
|
||||
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 generate_source(spec: &HarnessSpec) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
let (pre_call, call_expr) = build_call(spec, entry_fn);
|
||||
|
||||
format!(
|
||||
r#"<?php
|
||||
// Nyx dynamic harness — auto-generated, do not edit.
|
||||
|
||||
// ── Payload loading ────────────────────────────────────────────────────────────
|
||||
function nyx_payload(): string {{
|
||||
$v = getenv('NYX_PAYLOAD');
|
||||
if ($v !== false && $v !== '') {{
|
||||
return $v;
|
||||
}}
|
||||
$b64 = getenv('NYX_PAYLOAD_B64');
|
||||
if ($b64 !== false && $b64 !== '') {{
|
||||
return base64_decode($b64, true) ?: '';
|
||||
}}
|
||||
return '';
|
||||
}}
|
||||
|
||||
$payload = nyx_payload();
|
||||
|
||||
// ── Entry include ─────────────────────────────────────────────────────────────
|
||||
try {{
|
||||
require_once __DIR__ . '/entry.php';
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_IMPORT_ERROR: ' . $e->getMessage() . "\n");
|
||||
exit(77);
|
||||
}}
|
||||
|
||||
// ── Pre-call setup ─────────────────────────────────────────────────────────────
|
||||
{pre_call}
|
||||
// ── Call entry point ──────────────────────────────────────────────────────────
|
||||
try {{
|
||||
$result = {call_expr};
|
||||
if ($result !== null) {{
|
||||
echo $result . "\n";
|
||||
}}
|
||||
}} catch (Throwable $e) {{
|
||||
fwrite(STDERR, 'NYX_EXCEPTION: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||
}}
|
||||
"#,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, func: &str) -> (String, String) {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(idx) => {
|
||||
let pre = String::new();
|
||||
let call = if *idx == 0 {
|
||||
format!("{func}($payload)")
|
||||
} else {
|
||||
let pads = (0..*idx).map(|_| "''").collect::<Vec<_>>().join(", ");
|
||||
format!("{func}({pads}, $payload)")
|
||||
};
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
let pre = format!("putenv({name:?} . '=' . $payload);\n$_ENV[{name:?}] = $payload;\n");
|
||||
let call = format!("{func}()");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::Stdin => {
|
||||
// Replace STDIN with an in-memory stream containing the payload.
|
||||
let pre = "if (defined('STDIN')) {\n $stream = fopen('php://memory', 'r+');\n fwrite($stream, $payload);\n rewind($stream);\n // Note: STDIN reassignment is not portable; fixture reads via fgets(STDIN).\n}\n".to_owned();
|
||||
let call = format!("{func}()");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
let pre = String::new();
|
||||
let call = format!("{func}($payload)");
|
||||
(pre, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "php0000000000001".into(),
|
||||
entry_file: "src/login.php".into(),
|
||||
entry_name: "login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Php,
|
||||
toolchain_id: "php-8".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "src/login.php".into(),
|
||||
sink_line: 10,
|
||||
spec_hash: "php0000000000001".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.starts_with("<?php"));
|
||||
assert!(harness.source.contains("NYX_PAYLOAD"));
|
||||
assert!(harness.source.contains("require_once"));
|
||||
assert!(harness.source.contains("login($payload)"));
|
||||
assert_eq!(harness.filename, "harness.php");
|
||||
assert_eq!(harness.command, vec!["php", "harness.php"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_index_0() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("login($payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_index_2() {
|
||||
let spec = make_spec(PayloadSlot::Param(2));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("login('', '', $payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_env_var_slot() {
|
||||
let spec = make_spec(PayloadSlot::EnvVar("DB_HOST".into()));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("putenv"));
|
||||
assert!(harness.source.contains("\"DB_HOST\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_http_body_is_unsupported() {
|
||||
let spec = make_spec(PayloadSlot::HttpBody);
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_entry_subpath_is_entry_php() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("entry.php".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_has_base64_decode() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("base64_decode"));
|
||||
assert!(harness.source.contains("NYX_PAYLOAD_B64"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue