//! 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::PayloadSlotUnsupported`. //! //! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1). use crate::dynamic::lang::{HarnessSource, LangEmitter}; use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; use crate::evidence::UnsupportedReason; /// Zero-sized [`LangEmitter`] handle for Go. Method bodies delegate to the /// existing free functions in this module. pub struct GoEmitter; /// Entry kinds the Go emitter currently understands. Extended in Phase 15 /// (Track B Go vertical) to include `HttpRoute` (`net/http`, gin) and CLI /// (`flag.Parse`) shapes. const SUPPORTED: &[EntryKind] = &[EntryKind::Function]; impl LangEmitter for GoEmitter { fn emit(&self, spec: &HarnessSpec) -> Result { emit(spec) } fn entry_kinds_supported(&self) -> &'static [EntryKind] { SUPPORTED } fn entry_kind_hint(&self, attempted: EntryKind) -> String { format!( "go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add net/http, gin, flag.Parse shapes in phase 15" ) } } /// Source of the `__nyx_probe` shim for the Go harness (Phase 06 — /// Track C.1). Variadic over `string` so callers can pass any number of /// captured args at the sink site. pub fn probe_shim() -> &'static str { r#" // ── __nyx_probe shim (Phase 06 — Track C.1) ────────────────────────────────── func __nyx_probe(sinkCallee string, args ...string) { p := os.Getenv("NYX_PROBE_PATH") if p == "" { return } serArgs := make([]map[string]interface{}, 0, len(args)) for _, a := range args { serArgs = append(serArgs, map[string]interface{}{ "kind": "String", "value": a, }) } rec := map[string]interface{}{ "sink_callee": sinkCallee, "args": serArgs, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), } b, err := json.Marshal(rec) if err != nil { return } f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() f.Write(b) f.Write([]byte("\n")) } "# } /// Emit a Go harness for `spec`. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {} _ => return Err(UnsupportedReason::PayloadSlotUnsupported), } 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::() + 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(), derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps, } } #[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::PayloadSlotUnsupported); } #[test] fn emit_stdin_is_unsupported() { let spec = make_spec(PayloadSlot::Stdin); let err = emit(&spec).unwrap_err(); assert_eq!(err, UnsupportedReason::PayloadSlotUnsupported); } #[test] fn entry_kinds_supported_is_non_empty() { assert!(!GoEmitter.entry_kinds_supported().is_empty()); assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::Function)); } #[test] fn entry_kind_hint_names_attempted_and_phase() { let hint = GoEmitter.entry_kind_hint(EntryKind::HttpRoute); assert!(hint.contains("HttpRoute")); assert!(hint.contains("phase 15")); } #[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")); } }