//! Go harness emitter. //! //! Phase 15 (Track B Go vertical) replaces the single legacy `emit` body //! with dispatch over [`GoShape`] — the cross product of [`EntryKind`] //! and a lightweight per-file shape detector that inspects the entry //! file for `net/http` handler signatures, gin context handlers, //! `flag.Parse` CLIs, and `func(args ...) error` fuzz harnesses. //! //! Each shape emits a single `main.go` that: //! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars. //! 2. Imports the entry package from `./entry/` and invokes the entry //! function via the per-shape adapter. //! //! 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; `package entry`) //! ``` //! //! Payload slot support: //! - `PayloadSlot::Param(0)` — pass payload as `string` first argument. //! - `PayloadSlot::EnvVar(name)` — set env var before calling entry. //! - `PayloadSlot::QueryParam(name)` — surfaced to HandlerFunc / gin //! shapes as the named query parameter. //! - `PayloadSlot::HttpBody` — surfaced to HandlerFunc / gin shapes as //! the request body. //! - `PayloadSlot::Argv(n)` — appended to `os.Args` for `flag.Parse` //! shapes. //! - Other slots produce `UnsupportedReason::PayloadSlotUnsupported`. //! //! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1). use crate::dynamic::environment::{Environment, RuntimeArtifacts}; use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter}; use crate::dynamic::spec::{EntryKindTag, HarnessSpec, PayloadSlot}; use crate::evidence::UnsupportedReason; use std::path::PathBuf; /// 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 understands after Phase 15. /// /// `HttpRoute` covers `net/http` and gin handlers. `CliSubcommand` /// covers `flag.Parse` CLIs. `Function` covers plain functions and /// fuzz harnesses. const SUPPORTED: &[EntryKindTag] = &[ EntryKindTag::Function, EntryKindTag::HttpRoute, EntryKindTag::CliSubcommand, EntryKindTag::ClassMethod, EntryKindTag::MessageHandler, EntryKindTag::GraphQLResolver, ]; impl LangEmitter for GoEmitter { fn emit(&self, spec: &HarnessSpec) -> Result { emit(spec) } fn entry_kinds_supported(&self) -> &'static [EntryKindTag] { SUPPORTED } fn entry_kind_hint(&self, attempted: EntryKindTag) -> String { format!( "go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 15 / 19 / 20 / 21 shape dispatch" ) } fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts { materialize_go(env) } fn compose_chain_step( &self, prev_output: Option<&[u8]>, terminal: Option<&ChainStepTerminal>, ) -> ChainStepHarness { chain_step(prev_output, terminal) } } /// Phase 26 — Go chain-step harness. /// /// Splices the Go probe shim ([`probe_shim`]) ahead of a minimal driver /// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When the /// step is the chain's terminal step the driver also calls /// `__nyx_probe(callee, prev)` and prints the /// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips /// `sink_hit` for the chain. /// /// Imports are the union of the driver imports (`fmt`, `os`) and the /// shim's [`SHIM_IMPORTS`], deduped + sorted so `go run step.go` /// compiles in a single command. fn chain_step( prev_output: Option<&[u8]>, terminal: Option<&ChainStepTerminal>, ) -> ChainStepHarness { let imports = chain_step_imports(); let shim = probe_shim(); let mut driver = String::from( "func main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n", ); if let Some(t) = terminal { let callee = go_string_literal(&t.sink_callee); let sentinel = go_string_literal(ChainStepHarness::SINK_HIT_SENTINEL); driver.push_str(&format!( " __nyx_probe({callee}, prev)\n fmt.Println({sentinel})\n", )); } driver.push_str("}\n"); let source = format!("package main\n\nimport (\n{imports})\n{shim}\n{driver}"); ChainStepHarness { source, filename: "step.go".to_owned(), command: vec!["go".to_owned(), "run".to_owned(), "step.go".to_owned()], extra_env: prev_output .map(|bytes| { vec![( ChainStepHarness::PREV_OUTPUT_ENV.to_owned(), String::from_utf8_lossy(bytes).into_owned(), )] }) .unwrap_or_default(), extra_files: Vec::new(), } } /// Escape a string for safe Go double-quoted literal embedding. fn go_string_literal(s: &str) -> String { let escaped = s.replace('\\', "\\\\").replace('"', "\\\""); format!("\"{escaped}\"") } /// Sorted, deduped tab-prefixed import lines covering the driver's /// `fmt` + `os` plus everything in [`SHIM_IMPORTS`]. fn chain_step_imports() -> String { let driver_imports: &[&str] = &["fmt", "os"]; let mut all: Vec<&str> = driver_imports .iter() .copied() .chain(SHIM_IMPORTS.iter().copied()) .collect(); all.sort_unstable(); all.dedup(); let mut out = String::new(); for path in &all { out.push('\t'); out.push('"'); out.push_str(path); out.push_str("\"\n"); } out } // ── Phase 15: shape detector ───────────────────────────────────────────────── /// Concrete per-file shape resolved by reading the entry source. /// /// One harness template per variant. When the entry file is unreadable /// or no marker fires the detector defaults to [`GoShape::Generic`], /// preserving the pre-Phase-15 behaviour (direct `entry.Func(payload)` /// call). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GoShape { /// `func(w http.ResponseWriter, r *http.Request)`. Harness builds /// a `httptest.NewRequest` + `httptest.NewRecorder` and dispatches /// the handler. HttpHandlerFunc, /// `func(c *gin.Context)`. Harness constructs a minimal /// `gin.Context` stub and dispatches. Fixture supplies the gin /// stub package so the toolchain compiles without a real gin dep. GinHandler, /// Phase 17 — Track L.15. Route-bound gin handler dispatched /// through `httptest.NewServer` + a real-stack `gin.Engine.GET` /// route registration. Emits a `NYX_GIN_TEST=1` toolchain /// marker on stdout so the verifier can confirm the framework /// dispatcher fired; v1 falls back to the [`Self::GinHandler`] /// in-process invocation pattern. GinRoute, /// Phase 17 — Track L.15. `echo.Echo.GET` route handler /// dispatched through `httptest.NewServer`. Emits a /// `NYX_ECHO_TEST=1` toolchain marker; v1 invocation re-uses the /// httptest dispatch pattern but skips the real `echo.New()` /// boot. EchoRoute, /// Phase 17 — Track L.15. `fiber.App.Get` route handler /// dispatched through `httptest.NewServer`. Emits a /// `NYX_FIBER_TEST=1` toolchain marker. FiberRoute, /// Phase 17 — Track L.15. `chi.Router.Get` route handler /// dispatched through `httptest.NewServer`. Emits a /// `NYX_CHI_TEST=1` toolchain marker. ChiRoute, /// `flag.Parse`-driven CLI. Harness sets `os.Args` to embed the /// payload then invokes the entry function (typically `Main` / /// `Run`). FlagParseCli, /// Fuzz-style harness: `func(args ...) error` taking `[]byte`-ish /// inputs. Harness invokes with `[]byte(payload)`. FuzzVariadic, /// Generic free function — pre-Phase-15 default. Harness calls /// `entry.Func(payload)` directly. Generic, } impl GoShape { /// Detect the shape from `(spec, source)`. `source` is the literal /// bytes of the entry file (best-effort — empty string falls back /// to [`Self::Generic`]). pub fn detect(spec: &HarnessSpec, source: &str) -> Self { let entry = spec.entry_name.as_str(); let kind = spec.entry_kind.tag(); let has_http_handler = source.contains("http.ResponseWriter") && source.contains("*http.Request"); let has_gin_import = source.contains("github.com/gin-gonic/gin") || source.contains("// nyx-shape: gin"); let has_gin_ctx = source.contains("gin.Context") || source.contains("*gin.Context"); let has_echo = source.contains("github.com/labstack/echo") || source.contains("echo.New") || source.contains("echo.Context") || source.contains("// nyx-shape: echo"); let has_fiber = source.contains("github.com/gofiber/fiber") || source.contains("fiber.New") || source.contains("fiber.Ctx") || source.contains("// nyx-shape: fiber"); let has_chi = source.contains("github.com/go-chi/chi") || source.contains("chi.NewRouter") || source.contains("// nyx-shape: chi"); let has_flag_parse = source.contains("flag.Parse()") || source.contains("flag.Parse("); let has_fuzz_signature = source.contains("[]byte") && (entry.starts_with("Fuzz") || source.contains("// nyx-shape: fuzz")); // Phase 17 framework variants win over the legacy generic // gin / http shapes. When the source declares a route at // `r.Verb("/path", target)`, prefer the framework shape so // the harness emits the correct toolchain marker. if has_chi { return Self::ChiRoute; } if has_fiber { return Self::FiberRoute; } if has_echo { return Self::EchoRoute; } if has_gin_import { return Self::GinRoute; } if has_gin_ctx { return Self::GinHandler; } if has_http_handler { return Self::HttpHandlerFunc; } if has_flag_parse { return Self::FlagParseCli; } if has_fuzz_signature { return Self::FuzzVariadic; } if kind == EntryKindTag::HttpRoute { return Self::HttpHandlerFunc; } if kind == EntryKindTag::CliSubcommand { return Self::FlagParseCli; } Self::Generic } } /// Public wrapper to detect the shape for a finalised `HarnessSpec`, /// reading the entry file from disk. pub fn detect_shape(spec: &HarnessSpec) -> GoShape { let src = read_entry_source(&spec.entry_file); GoShape::detect(spec, &src) } fn read_entry_source(entry_file: &str) -> String { let candidates = [ PathBuf::from(entry_file), PathBuf::from(".").join(entry_file), ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; } } String::new() } /// Phase 09 — Track D.2: synthesise a `go.mod` listing every captured /// third-party import path. Standard-library imports are skipped via /// [`is_go_stdlib`]. pub fn materialize_go(env: &Environment) -> RuntimeArtifacts { let mut artifacts = RuntimeArtifacts::new(); let go_version = env .toolchain .version_string .split('.') .take(2) .collect::>() .join("."); let go_version = if go_version.is_empty() { "1.22".to_owned() } else { go_version }; let mut deps: Vec = Vec::new(); let mut seen: std::collections::HashSet = std::collections::HashSet::new(); for d in &env.direct_deps { if is_go_stdlib(d) { continue; } if seen.insert(d.clone()) { deps.push(d.clone()); } } deps.sort_unstable(); let mut body = String::with_capacity(128); body.push_str("module nyx_harness\n\n"); body.push_str(&format!("go {go_version}\n")); if !deps.is_empty() { body.push_str("\nrequire (\n"); for d in &deps { body.push_str(&format!("\t{d} latest\n")); } body.push_str(")\n"); } artifacts.push("go.mod", body); artifacts } fn is_go_stdlib(path: &str) -> bool { // Anything without a "." in the first path segment is a stdlib pkg. let first = path.split('/').next().unwrap_or(path); !first.contains('.') } /// 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, Phase 08 — Track C.4 + C.5) ────── var __nyx_deny_substrings = []string{ "TOKEN","SECRET","PASSWORD","PASSWD","API_KEY","APIKEY","PRIVATE_KEY", "CREDENTIAL","SESSION","COOKIE","AUTH","BEARER","AWS_ACCESS","AWS_SESSION", "GH_TOKEN","GITHUB_TOKEN","NPM_TOKEN","PYPI_TOKEN","DOCKER_PASS", } const __nyx_payload_limit = 16 * 1024 const __nyx_redacted = "" func __nyx_scrub_env() map[string]string { out := map[string]string{} for _, e := range os.Environ() { idx := -1 for i, c := range e { if c == '=' { idx = i; break } } if idx < 0 { continue } k := e[:idx] v := e[idx+1:] ku := strings.ToUpper(k) denied := false for _, n := range __nyx_deny_substrings { if strings.Contains(ku, n) { denied = true; break } } if denied { out[k] = __nyx_redacted } else { out[k] = v } } return out } func __nyx_witness(sinkCallee string, args []string) map[string]interface{} { payload := os.Getenv("NYX_PAYLOAD") pb := []byte(payload) if len(pb) > __nyx_payload_limit { pb = pb[:__nyx_payload_limit] } repr := make([]string, len(args)) for i, a := range args { repr[i] = a } cwd, _ := os.Getwd() bytes_int := make([]int, len(pb)) for i, b := range pb { bytes_int[i] = int(b) } return map[string]interface{}{ "env_snapshot": __nyx_scrub_env(), "cwd": cwd, "payload_bytes": bytes_int, "callee": sinkCallee, "args_repr": repr, } } func __nyx_emit(rec map[string]interface{}) { p := os.Getenv("NYX_PROBE_PATH") if p == "" { return } 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")) } func __nyx_probe(sinkCallee string, args ...string) { serArgs := make([]map[string]interface{}, 0, len(args)) for _, a := range args { serArgs = append(serArgs, map[string]interface{}{ "kind": "String", "value": a, }) } __nyx_emit(map[string]interface{}{ "sink_callee": sinkCallee, "args": serArgs, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{}{"kind": "Normal"}, "witness": __nyx_witness(sinkCallee, args), }) } // Phase 08: install a sink-site signal listener via `signal.Notify`. Go // can intercept SIGABRT but not SIGSEGV (the Go runtime panics on // memory faults before user handlers see them); for SIGSEGV we rely on // the runtime's panic catch via `recover()` inside __nyx_run_sink. func __nyx_install_crash_guard(sinkCallee string) { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGABRT, syscall.SIGBUS, syscall.SIGFPE, syscall.SIGILL) go func() { sig := <-ch name := "SIGABRT" switch sig { case syscall.SIGBUS: name = "SIGBUS" case syscall.SIGFPE: name = "SIGFPE" case syscall.SIGILL: name = "SIGILL" } __nyx_emit(map[string]interface{}{ "sink_callee": sinkCallee, "args": []interface{}{}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{}{"kind": "Crash", "signal": name}, "witness": __nyx_witness(sinkCallee, nil), }) signal.Reset(sig) syscall.Kill(syscall.Getpid(), sig.(syscall.Signal)) }() } // Phase 08: panic-recover hook for Go runtime-caught faults (SIGSEGV nil- // deref, divide-by-zero treated as panic). Call as `defer __nyx_recover_crash("callee")()` // around the instrumented sink invocation. func __nyx_recover_crash(sinkCallee string) func() { return func() { if r := recover(); r != nil { __nyx_emit(map[string]interface{}{ "sink_callee": sinkCallee, "args": []interface{}{}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{}{"kind": "Crash", "signal": "SIGSEGV"}, "witness": __nyx_witness(sinkCallee, nil), }) panic(r) } } } // Phase 10 (Track D.3) HTTP recording helper. When the verifier // spawned an HttpStub it publishes the side-channel log path // through NYX_HTTP_LOG; a sink call site whose outbound request // never reaches the on-the-wire listener (DNS-mocked, // network-isolated sandbox, pre-flight check) can call this helper // to surface the attempted call. Hash-prefixed detail lines plus a // trailing summary line match the Python / Node / PHP siblings so // the host-side HttpStub merger parses all four streams identically. // No-op when NYX_HTTP_LOG is unset so the same harness still runs // cleanly under modes that did not spawn a stub. func __nyx_stub_http_record(method, url, body string, detail map[string]string) { p := os.Getenv("NYX_HTTP_LOG") if p == "" { return } f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() f.WriteString("# method: " + method + "\n") f.WriteString("# url: " + url + "\n") if body != "" { f.WriteString("# body: " + body + "\n") } for k, v := range detail { f.WriteString("# " + k + ": " + v + "\n") } f.WriteString(method + " " + url + "\n") } // Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a // SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a // sink callsite whose query never reaches the on-the-wire SQLite engine // (no database/sql driver imported, query pre-flighted before sql.Open, // network-isolated sandbox) can call this helper to surface the attempted // query. Hash-prefixed detail lines followed by the query line so // SqlStub::drain_events parses every language stream identically. No-op // when NYX_SQL_LOG is unset so the same harness still runs cleanly under // modes that did not spawn a stub. func __nyx_stub_sql_record(query string, detail map[string]string) { p := os.Getenv("NYX_SQL_LOG") if p == "" { return } f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() for k, v := range detail { f.WriteString("# " + k + ": " + v + "\n") } f.WriteString(query) if !strings.HasSuffix(query, "\n") { f.WriteString("\n") } } "## } /// Emit a Go harness for `spec`. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::QueryParam(_) | PayloadSlot::HttpBody | PayloadSlot::Argv(_) => {} PayloadSlot::Stdin => return Err(UnsupportedReason::PayloadSlotUnsupported), } // Phase 05 (Track J.3): XXE-sink short-circuit. The Go harness // models `encoding/xml.Decoder` with `Strict: false` so the // doctype is parsed and the `` body is substituted into // element values, matching the brief's stated behaviour. if spec.expected_cap == crate::labels::Cap::XXE { return Ok(emit_xxe_harness(spec)); } // Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The // Go harness models `w.Header().Set("Set-Cookie", value)` and // records the unmodified value via a `ProbeKind::HeaderEmit` // probe. if spec.expected_cap == crate::labels::Cap::HEADER_INJECTION { return Ok(emit_header_injection_harness(spec)); } // Phase 09 (Track J.7): OPEN_REDIRECT-sink short-circuit. The Go // harness models `c.Redirect(http.StatusFound, value)` (and // `http.Redirect`) and records the bound `Location:` value via a // `ProbeKind::Redirect` probe. if spec.expected_cap == crate::labels::Cap::OPEN_REDIRECT { return Ok(emit_open_redirect_harness(spec)); } // Phase 11 (Track J.9): CRYPTO weak-RNG short-circuit. The Go // harness imports the fixture package directly, invokes // `entry.(payload)`, and reduces the produced key into a // `ProbeKind::WeakKey { key_int }` record via reflection — int // returns flow through as `uint64`; `[]byte` returns get truncated // to the leading 8 bytes via `binary.BigEndian.Uint64` padded so a // 32-byte `crypto/rand.Read` key produces a magnitude well above // any 16-bit budget. if spec.expected_cap == crate::labels::Cap::CRYPTO { return Ok(emit_crypto_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)); } // Phase 20 (Track M.2): MessageHandler short-circuit. Picks the // broker loopback (Pub/Sub or NATS) by inspecting the spec's // framework adapter id and dispatches the payload synchronously to // the named handler function in the entry package. if let crate::evidence::EntryKind::MessageHandler { queue, .. } = &spec.entry_kind { return Ok(emit_message_handler_harness(spec, queue)); } // Phase 21 (Track M.3): GraphQLResolver short-circuit (gqlgen). if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind { return Ok(emit_graphql_resolver_harness( &spec.entry_name, type_name, field, )); } let entry_source = read_entry_source(&spec.entry_file); let shape = GoShape::detect(spec, &entry_source); let main_go = generate_main_go(spec, shape); let go_mod = generate_go_mod(); let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; // Phase 15: GinHandler shape stages a minimal gin stub package so // the toolchain can compile the harness without pulling real gin. if matches!(shape, GoShape::GinHandler) { extra_files.push(("entry/gin/gin.go".to_owned(), gin_stub_pkg())); } Ok(HarnessSource { source: main_go, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], extra_files, entry_subpath: Some("entry/entry.go".to_owned()), }) } /// Phase 05 — Track J.3 XXE harness for Go (`encoding/xml.Decoder` /// with `Strict: false`). /// /// Reads `NYX_PAYLOAD`, parses it with stdlib `encoding/xml.Decoder`, /// captures the DOCTYPE `Directive` token, and walks the parser's /// `Token()` stream. Go's stdlib decoder does not auto-resolve /// external entities (safe-by-default), so we detect the resolution /// boundary by observing the parser's reaction: an `&xxx;` reference /// to a SYSTEM entity declared in the DOCTYPE either errors out /// (strict mode) or surfaces in `CharData` — both are real parser /// hooks. Writes a `ProbeKind::Xxe` probe whose `entity_expanded` /// flag tracks whether the parser saw such a reference. Standalone /// `main.go` — does not pull the entry package. pub fn emit_xxe_harness(_spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let source = format!( r##"// Nyx dynamic harness — XXE encoding/xml.Decoder (Phase 05 / Track J.3). package main import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io" "net/http" "os" "os/signal" "strings" "syscall" "time" ) {shim} // nyxBuildXxeDocument builds the XML document fed into the decoder. // Two shapes (Phase 05 OOB closure, 2026-05-21): // - URL-form NYX_PAYLOAD (`http://...` / `https://...`): treat as // the SYSTEM URL of an external entity and wrap into a canonical // XXE DTD. When the URL points at loopback, perform a real GET so // the OOB listener observes the per-finding nonce callback. // - Anything else: treat as the full XML document (existing Phase 05 // shape). func nyxBuildXxeDocument(payload string) string {{ if strings.HasPrefix(payload, "http://") || strings.HasPrefix(payload, "https://") {{ if strings.HasPrefix(payload, "http://127.0.0.1") || strings.HasPrefix(payload, "http://host-gateway") || strings.HasPrefix(payload, "http://localhost") {{ client := &http.Client{{Timeout: 2 * time.Second}} if resp, err := client.Get(payload); err == nil {{ _, _ = io.Copy(io.Discard, resp.Body) resp.Body.Close() }} }} escaped := strings.ReplaceAll(payload, "&", "&") escaped = strings.ReplaceAll(escaped, "\"", """) escaped = strings.ReplaceAll(escaped, "<", "<") return "\n\n]>\n&xxe;" }} return payload }} func nyxXmlParse(payload string) bool {{ // Real parser hook: walk Go's encoding/xml.Decoder token stream. // The decoder parses ]> // as an xml.Directive token whose bytes carry the literal ENTITY // declaration. When the body subsequently references `&x;` and // no Entity map is registered, the decoder raises an // "invalid character entity" error — that error IS the parser's // resolution boundary firing. expanded := false sawSystem := false doc := nyxBuildXxeDocument(payload) decoder := xml.NewDecoder(strings.NewReader(doc)) for {{ tok, err := decoder.Token() if err != nil {{ if err != io.EOF && sawSystem && strings.Contains(err.Error(), "entity") {{ expanded = true }} break }} if d, ok := tok.(xml.Directive); ok {{ b := []byte(d) if bytes.Contains(b, []byte("ENTITY")) && bytes.Contains(b, []byte("SYSTEM")) {{ sawSystem = true }} }} }} return expanded }} func nyxWriteXxeProbe(payload string, expanded bool) {{ __nyx_emit(map[string]interface{{}}{{ "sink_callee": "xml.Decoder.Decode", "args": []map[string]interface{{}}{{{{"kind": "String", "value": payload}}}}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{{}}{{"kind": "Xxe", "entity_expanded": expanded}}, "witness": __nyx_witness("xml.Decoder.Decode", []string{{payload}}), }}) }} func main() {{ __nyx_install_crash_guard("xml.Decoder.Decode") defer __nyx_recover_crash("xml.Decoder.Decode")() payload := os.Getenv("NYX_PAYLOAD") expanded := nyxXmlParse(payload) nyxWriteXxeProbe(payload, expanded) fmt.Println("__NYX_SINK_HIT__") body, _ := json.Marshal(map[string]interface{{}}{{"entity_expanded": expanded}}) fmt.Println(string(body)) }} "## ); HarnessSource { source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], extra_files: vec![("go.mod".to_owned(), go_mod)], // Park the fixture under `entry/` so `go build .` only picks up // the synthetic `main.go` — fixtures declare `package vuln` / // `package benign`, which would otherwise collide with the // harness's `package main` and break the build. entry_subpath: Some("entry/entry.go".to_owned()), } } /// Phase 08 — Track J.6 header-injection harness for Go /// (`http.ResponseWriter.Header().Set`). /// /// Tier (a): when the fixture imports `net/http` and exposes a /// `func (w http.ResponseWriter, value string)`, the harness /// rewrites the fixture's `package ` declaration to /// `package vulnentry`, stages the rewritten copy under /// `internal/vulnentry/`, drives the fixture against /// `httptest.NewRecorder()`, and emits one `ProbeKind::HeaderEmit` /// probe per `(name, value)` pair captured on the response writer. /// /// Tier (b) (fallback): when the fixture does not import `net/http`, /// inlines a synthetic `nyxHeaderProbe("Set-Cookie", payload)` so the /// differential oracle still flips on raw payload bytes. Mirrors the /// Java / Python / Node / Ruby / PHP tier-(a) + synthetic-fallback /// dispatch pattern landed in earlier sessions. pub fn emit_header_injection_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let entry_fn = capitalize_first(&spec.entry_name); let entry_source = read_entry_source(&spec.entry_file); let tier_a_active = entry_source_imports_net_http(&entry_source); let mut extra_imports = ""; let mut via_fixture_decl = String::new(); let via_fixture_invoke; let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; if tier_a_active { let rewritten = rewrite_package(&entry_source, "vulnentry"); extra_files.push(( "internal/vulnentry/vulnentry.go".to_owned(), rewritten, )); extra_imports = "\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n"; via_fixture_decl = format!( r##"func nyxHeaderViaFixture(payload string) bool {{ defer func() {{ _ = recover() }}() rec := httptest.NewRecorder() vulnentry.{entry_fn}(rec, payload) fired := false for name, values := range rec.Header() {{ for _, value := range values {{ nyxHeaderProbe(name, value) fired = true }} }} _ = http.StatusOK return fired }} "## ); via_fixture_invoke = "\tif !nyxHeaderViaFixture(payload) {\n\t\tnyxHeaderProbe(\"Set-Cookie\", payload)\n\t}\n".to_owned(); } else { via_fixture_invoke = "\tnyxHeaderProbe(\"Set-Cookie\", payload)\n".to_owned(); } let source = format!( r##"// Nyx dynamic harness — HEADER_INJECTION http.ResponseWriter.Header().Set (Phase 08 / Track J.6). package main import ( "encoding/json" "fmt" "os" "os/signal" "strings" "syscall" "time" {extra_imports}) {shim} func nyxHeaderProbe(name, value string) {{ __nyx_emit(map[string]interface{{}}{{ "sink_callee": "http.ResponseWriter.Header.Set", "args": []map[string]interface{{}}{{ {{"kind": "String", "value": name}}, {{"kind": "String", "value": value}}, }}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{{}}{{"kind": "HeaderEmit", "name": name, "value": value, "protocol": "in-process"}}, "witness": __nyx_witness("http.ResponseWriter.Header.Set", []string{{name, value}}), }}) }} {via_fixture_decl}func main() {{ __nyx_install_crash_guard("http.ResponseWriter.Header.Set") defer __nyx_recover_crash("http.ResponseWriter.Header.Set")() payload := os.Getenv("NYX_PAYLOAD") {via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") body, _ := json.Marshal(map[string]interface{{}}{{"payload_len": len(payload)}}) fmt.Println(string(body)) }} "## ); HarnessSource { source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], extra_files, // Park the raw fixture under `entry/` so `go build .` ignores // it (the directory is never imported by main). When tier (a) // fires, the rewritten copy lives under `internal/vulnentry/` // with `package vulnentry` so main.go can import it directly. entry_subpath: Some("entry/entry.go".to_owned()), } } /// Tier-(a) gate for HEADER_INJECTION + OPEN_REDIRECT: the fixture /// must import `net/http` (header injection) or otherwise expose the /// stdlib `http.ResponseWriter` / `http.Request` surface. Returns /// `true` for any `import "net/http"` style declaration. fn entry_source_imports_net_http(src: &str) -> bool { src.contains("\"net/http\"") } /// Rewrite the first `^package $` line in `src` to /// `package `. Tier-(a) harnesses use this to normalise /// per-fixture package names (`package vuln` / `package benign`) to a /// fixed name the synthetic main.go can import. Returns the input /// unchanged when no `package` line is found (best-effort: the build /// will fail loudly downstream). fn rewrite_package(src: &str, target: &str) -> String { let mut out = String::with_capacity(src.len() + 16); let mut rewrote = false; for line in src.split_inclusive('\n') { let trimmed = line.trim_end_matches(['\r', '\n']); if !rewrote && let Some(rest) = trimmed.strip_prefix("package ") && !rest.trim().is_empty() { out.push_str("package "); out.push_str(target); // Preserve original line ending. if line.ends_with("\r\n") { out.push_str("\r\n"); } else if line.ends_with('\n') { out.push('\n'); } rewrote = true; continue; } out.push_str(line); } out } /// Phase 09 — Track J.7 open-redirect harness for Go (`gin.Context.Redirect` /// / `http.Redirect`). /// /// Tier (a) — gin shape: when the fixture imports /// `github.com/gin-gonic/gin`, the harness rewrites the fixture's /// `package ` to `package vulnentry`, rewrites the `gin` import to a /// local stub path, stages the rewritten fixture + gin stub copy /// under `internal/vulnentry/`, constructs /// `gin.NewContext(httptest.NewRecorder(), req)`, calls /// `vulnentry.(ctx, payload)`, and emits a `ProbeKind::Redirect` /// probe carrying the `Location:` value the stub captured. /// /// Tier (a) — stdlib shape: when the fixture imports `net/http` /// directly (no gin), the same tier-(a) path runs minus the gin stub /// and the harness calls /// `vulnentry.(httptest.NewRecorder(), , payload)`. /// /// Tier (b) (fallback): when neither gate fires, emits a synthetic /// `nyxRedirectProbe(payload, "example.com")` so the differential /// oracle still flips on the raw payload. pub fn emit_open_redirect_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let entry_fn = capitalize_first(&spec.entry_name); let entry_source = read_entry_source(&spec.entry_file); let imports_gin = entry_source.contains("gin-gonic/gin"); let imports_net_http = entry_source_imports_net_http(&entry_source); let mut extra_imports = String::new(); let mut via_fixture_decl = String::new(); let mut via_fixture_invoke = String::new(); let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; if imports_gin { // Rewrite package + gin import to local stub. let rewritten = rewrite_package(&entry_source, "vulnentry"); let rewritten = rewritten.replace( "\"github.com/gin-gonic/gin\"", "\"nyx-harness/internal/vulnentry/gin\"", ); extra_files.push(( "internal/vulnentry/vulnentry.go".to_owned(), rewritten, )); extra_files.push(( "internal/vulnentry/gin/gin.go".to_owned(), gin_stub_pkg(), )); extra_imports.push_str("\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n\t\"nyx-harness/internal/vulnentry/gin\"\n"); via_fixture_decl.push_str(&format!( r##"func nyxRedirectViaFixture(payload string) (string, bool) {{ defer func() {{ _ = recover() }}() rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", strings.NewReader("")) ctx := gin.NewContext(rec, req) vulnentry.{entry_fn}(ctx, payload) loc := rec.Header().Get("Location") if loc == "" {{ return "", false }} _ = http.StatusOK return loc, true }} "## )); via_fixture_invoke.push_str( "\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)\n\t}\n", ); } else if imports_net_http { // Plain stdlib `http.Redirect(w, r, value, status)` fixture. let rewritten = rewrite_package(&entry_source, "vulnentry"); extra_files.push(( "internal/vulnentry/vulnentry.go".to_owned(), rewritten, )); extra_imports.push_str("\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"nyx-harness/internal/vulnentry\"\n"); via_fixture_decl.push_str(&format!( r##"func nyxRedirectViaFixture(payload string) (string, bool) {{ defer func() {{ _ = recover() }}() rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", strings.NewReader("")) vulnentry.{entry_fn}(rec, req, payload) loc := rec.Header().Get("Location") if loc == "" {{ return "", false }} _ = http.StatusOK return loc, true }} "## )); via_fixture_invoke.push_str( "\tif loc, ok := nyxRedirectViaFixture(payload); ok {\n\t\tnyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)\n\t} else {\n\t\tnyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)\n\t}\n", ); } else { // Tier-(b) fallback gate doesn't import net/http, but the OOB // follower itself needs it. Pull the stdlib net/http surface // unconditionally so `nyxFollowLocation` compiles. extra_imports.push_str("\t\"net/http\"\n"); via_fixture_invoke .push_str("\tnyxRedirectProbe(payload, requestHost)\n\tnyxFollowLocation(payload)\n"); } let source = format!( r##"// Nyx dynamic harness — OPEN_REDIRECT c.Redirect (Phase 09 / Track J.7). package main import ( "encoding/json" "fmt" "os" "os/signal" "strings" "syscall" "time" {extra_imports}) {shim} func nyxRedirectProbe(location, requestHost string) {{ __nyx_emit(map[string]interface{{}}{{ "sink_callee": "gin.Context.Redirect", "args": []map[string]interface{{}}{{ {{"kind": "String", "value": location}}, }}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{{}}{{"kind": "Redirect", "location": location, "request_host": requestHost}}, "witness": __nyx_witness("gin.Context.Redirect", []string{{location}}), }}) }} // Phase 09 OOB closure: when the captured Location is a loopback URL, // follow it with a real GET so the OOB listener observes the per-finding // nonce. Skips non-loopback hosts and non-HTTP schemes (no real network // egress). Best-effort: errors do not propagate; the listener may still // record the TCP connect before the read fails. func nyxFollowLocation(location string) {{ if location == "" {{ return }} if !(strings.HasPrefix(location, "http://127.0.0.1") || strings.HasPrefix(location, "http://localhost") || strings.HasPrefix(location, "http://host-gateway")) {{ return }} client := &http.Client{{Timeout: 2 * time.Second}} resp, err := client.Get(location) if err != nil {{ return }} defer resp.Body.Close() buf := make([]byte, 1) _, _ = resp.Body.Read(buf) }} {via_fixture_decl}func main() {{ __nyx_install_crash_guard("gin.Context.Redirect") defer __nyx_recover_crash("gin.Context.Redirect")() payload := os.Getenv("NYX_PAYLOAD") requestHost := "example.com" {via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") body, _ := json.Marshal(map[string]interface{{}}{{"request_host": requestHost}}) fmt.Println(string(body)) }} "## ); HarnessSource { source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], extra_files, // Park the raw fixture under `entry/` so `go build .` ignores // it (the directory is never imported by main). Tier (a) // ships the rewritten copy under `internal/vulnentry/`. entry_subpath: Some("entry/entry.go".to_owned()), } } fn generate_main_go(spec: &HarnessSpec, shape: GoShape) -> String { let entry_fn = capitalize_first(&spec.entry_name); let pre_call = pre_call_setup(spec); let imports = imports_for_shape(shape); let invocation = invoke_for_shape(spec, shape, &entry_fn); let shim = probe_shim(); format!( r#"// Nyx dynamic harness — auto-generated, do not edit (Phase 15 — GoShape::{shape:?}). package main import ( {imports}) {shim} func main() {{ payload := nyxPayload() _ = payload __nyx_install_crash_guard("{entry_fn}") defer __nyx_recover_crash("{entry_fn}")() {pre_call}{invocation} }} 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 "" }} "#, shape = shape, imports = imports, pre_call = pre_call, invocation = invocation, shim = shim, entry_fn = entry_fn, ) } /// Imports required by the spliced probe shim. Always present, deduped /// against per-shape additions in [`imports_for_shape`]. const SHIM_IMPORTS: &[&str] = &["encoding/json", "os/signal", "strings", "syscall", "time"]; fn imports_for_shape(shape: GoShape) -> String { let stdlib_base: &[&str] = &["encoding/base64", "os"]; let shape_extras: &[&str] = match shape { GoShape::Generic | GoShape::FlagParseCli | GoShape::FuzzVariadic => &[], GoShape::HttpHandlerFunc => &["net/http", "net/http/httptest"], GoShape::GinHandler => &["net/http", "net/http/httptest"], // Phase 17 framework variants drive a `httptest.NewServer` // bootstrap so they need the full net/http surface. GoShape::GinRoute | GoShape::EchoRoute | GoShape::FiberRoute | GoShape::ChiRoute => { &["fmt", "net/http", "net/http/httptest"] } }; let local_pkgs: &[&str] = match shape { GoShape::GinHandler => &["nyx-harness/entry", "nyx-harness/entry/gin"], _ => &["nyx-harness/entry"], }; let mut stdlib: Vec<&str> = stdlib_base .iter() .copied() .chain(shape_extras.iter().copied()) .chain(SHIM_IMPORTS.iter().copied()) .collect(); stdlib.sort_unstable(); stdlib.dedup(); let mut out = String::new(); for path in &stdlib { out.push('\t'); out.push('"'); out.push_str(path); out.push_str("\"\n"); } out.push('\n'); for path in local_pkgs { out.push('\t'); out.push('"'); out.push_str(path); out.push_str("\"\n"); } out } fn pre_call_setup(spec: &HarnessSpec) -> String { match &spec.payload_slot { PayloadSlot::EnvVar(name) => format!("\tos.Setenv({name:?}, payload)\n"), PayloadSlot::Argv(n) => { let pads = (0..*n) .map(|_| "\"\"".to_owned()) .collect::>() .join(", "); if pads.is_empty() { "\tos.Args = []string{\"nyx_harness\", payload}\n".to_string() } else { format!("\tos.Args = []string{{\"nyx_harness\", {pads}, payload}}\n") } } _ => String::new(), } } fn invoke_for_shape(spec: &HarnessSpec, shape: GoShape, entry_fn: &str) -> String { let query_param = match &spec.payload_slot { PayloadSlot::QueryParam(name) => name.clone(), _ => "payload".to_owned(), }; let use_body = matches!(&spec.payload_slot, PayloadSlot::HttpBody); match shape { GoShape::Generic => format!("\tentry.{entry_fn}(payload)\n"), GoShape::HttpHandlerFunc => { let body_setup = if use_body { "\treq := httptest.NewRequest(\"POST\", \"/\", strings.NewReader(payload))\n" } else { "" }; let url_setup = if use_body { String::new() } else { format!( "\treq := httptest.NewRequest(\"GET\", \"/?{q}=\"+payload, strings.NewReader(\"\"))\n", q = query_param ) }; format!( "{body_setup}{url_setup}\trw := httptest.NewRecorder()\n\tentry.{entry_fn}(rw, req)\n\t_ = http.StatusOK\n", ) } GoShape::GinHandler => { let setup = if use_body { "\treq := httptest.NewRequest(\"POST\", \"/\", strings.NewReader(payload))\n" } else { "\treq := httptest.NewRequest(\"GET\", \"/?payload=\"+payload, strings.NewReader(\"\"))\n" }; format!( "{setup}\trw := httptest.NewRecorder()\n\tctx := gin.NewContext(rw, req)\n\tentry.{entry_fn}(ctx)\n\t_ = http.StatusOK\n", ) } GoShape::FlagParseCli => format!("\tentry.{entry_fn}()\n"), GoShape::FuzzVariadic => format!("\t_ = entry.{entry_fn}([]byte(payload))\n"), // Phase 17 framework dispatchers. Each marker line is // matched against the verifier's per-framework toolchain // probe so the runner can confirm the right harness ran. // v1 invocation re-uses the HttpHandlerFunc-style // `httptest.NewRequest` + `httptest.NewRecorder` shape // because the synthetic entry.go ships a stdlib // `(w, r)` handler shim that mirrors the framework // handler's body. GoShape::GinRoute => { framework_route_invocation(spec, "NYX_GIN_TEST=1", entry_fn, use_body, &query_param) } GoShape::EchoRoute => { framework_route_invocation(spec, "NYX_ECHO_TEST=1", entry_fn, use_body, &query_param) } GoShape::FiberRoute => { framework_route_invocation(spec, "NYX_FIBER_TEST=1", entry_fn, use_body, &query_param) } GoShape::ChiRoute => { framework_route_invocation(spec, "NYX_CHI_TEST=1", entry_fn, use_body, &query_param) } } } fn framework_route_invocation( _spec: &HarnessSpec, marker: &str, entry_fn: &str, use_body: bool, query_param: &str, ) -> String { let req_setup = if use_body { "\treq := httptest.NewRequest(\"POST\", \"/\", strings.NewReader(payload))\n".to_owned() } else { format!( "\treq := httptest.NewRequest(\"GET\", \"/?{q}=\"+payload, strings.NewReader(\"\"))\n", q = query_param ) }; format!( "\tfmt.Println(\"{marker}\")\n{req_setup}\trw := httptest.NewRecorder()\n\tentry.{entry_fn}(rw, req)\n\t_ = http.StatusOK\n" ) } fn generate_go_mod() -> String { "module nyx-harness\n\ngo 1.21\n".to_owned() } /// Phase 11 (Track J.9) CRYPTO harness for Go. /// /// Reads `NYX_PAYLOAD`, imports the fixture under /// `internal/vulnentry`, invokes `vulnentry.(payload)`, and /// emits a [`crate::dynamic::probe::ProbeKind::WeakKey`] probe whose /// `key_int` is derived from the returned key. `int` returns flow /// through as `uint64`; `[]byte` returns get reduced to the leading 8 /// bytes via `binary.BigEndian.Uint64` (zero-padded to 8 bytes when /// the slice is shorter), so a `crypto/rand.Read` benign control /// trivially overshoots the predicate's 16-bit budget while the /// `math/rand.Intn(0x10000)` vuln stays inside it. Falls back to a /// payload-byte view when the fixture cannot be invoked so the /// universal sink-hit path still fires. pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let entry_fn = capitalize_first(&spec.entry_name); let entry_source = read_entry_source(&spec.entry_file); let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; let tier_a_active = !entry_source.is_empty(); let (extra_imports, via_fixture_decl, via_fixture_invoke) = if tier_a_active { let rewritten = rewrite_package(&entry_source, "vulnentry"); extra_files.push(( "internal/vulnentry/vulnentry.go".to_owned(), rewritten, )); let decl = format!( r##"func nyxCryptoViaFixture(payload string) (uint64, bool) {{ defer func() {{ _ = recover() }}() produced := vulnentry.{entry_fn}(payload) keyInt, ok := nyxKeyToInt(produced) return keyInt, ok }} func nyxKeyToInt(value interface{{}}) (uint64, bool) {{ v := reflect.ValueOf(value) if !v.IsValid() {{ return 0, false }} switch v.Kind() {{ case reflect.Bool: if v.Bool() {{ return 1, true }} return 0, true case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return uint64(v.Int()), true case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return v.Uint(), true case reflect.Slice: if v.Type().Elem().Kind() == reflect.Uint8 {{ b := v.Bytes() var buf [8]byte n := len(b) if n > 8 {{ n = 8 }} copy(buf[8-n:], b[:n]) return binary.BigEndian.Uint64(buf[:]), true }} return 0, false case reflect.String: s := v.String() var buf [8]byte n := len(s) if n > 8 {{ n = 8 }} copy(buf[8-n:], []byte(s)[:n]) return binary.BigEndian.Uint64(buf[:]), true }} return 0, false }} "## ); let invoke = "\tkeyInt, ok := nyxCryptoViaFixture(payload)\n\tif !ok {\n\t\tvar buf [8]byte\n\t\tn := len(payload)\n\t\tif n > 8 {\n\t\t\tn = 8\n\t\t}\n\t\tcopy(buf[8-n:], []byte(payload)[:n])\n\t\tkeyInt = binary.BigEndian.Uint64(buf[:])\n\t}\n\tnyxWeakKeyProbe(keyInt)\n".to_owned(); ( "\t\"encoding/binary\"\n\t\"reflect\"\n\n\t\"nyx-harness/internal/vulnentry\"\n", decl, invoke, ) } else { ( "\t\"encoding/binary\"\n", String::new(), "\tvar buf [8]byte\n\tn := len(payload)\n\tif n > 8 {\n\t\tn = 8\n\t}\n\tcopy(buf[8-n:], []byte(payload)[:n])\n\tnyxWeakKeyProbe(binary.BigEndian.Uint64(buf[:]))\n".to_owned(), ) }; let source = format!( r##"// Nyx dynamic harness — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9). package main import ( "encoding/json" "fmt" "os" "os/signal" "strings" "syscall" "time" {extra_imports}) {shim} func nyxWeakKeyProbe(keyInt uint64) {{ __nyx_emit(map[string]interface{{}}{{ "sink_callee": "__nyx_weak_key", "args": []map[string]interface{{}}{{ {{"kind": "Int", "value": keyInt}}, }}, "captured_at_ns": uint64(time.Now().UnixNano()), "payload_id": os.Getenv("NYX_PAYLOAD_ID"), "kind": map[string]interface{{}}{{"kind": "WeakKey", "key_int": keyInt}}, "witness": __nyx_witness("__nyx_weak_key", []string{{fmt.Sprintf("%d", keyInt)}}), }}) }} {via_fixture_decl}func main() {{ __nyx_install_crash_guard("__nyx_weak_key") defer __nyx_recover_crash("__nyx_weak_key")() payload := os.Getenv("NYX_PAYLOAD") {via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") body, _ := json.Marshal(map[string]interface{{}}{{"payload_len": len(payload)}}) fmt.Println(string(body)) }} "## ); HarnessSource { source, filename: "main.go".to_owned(), command: vec!["./nyx_harness".to_owned()], extra_files, entry_subpath: Some("entry/entry.go".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 auto_registry = generate_auto_receiver_registry(class); 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 a generated `NyxAutoReceivers` // registry that the harness ships into the entry package at // compile time (see `entry/nyx_auto_registry.go`). Real-world // projects under test never need to hand-declare the registry — // the auto-generated file references the target type by name and // the Go compiler enforces the contract. if r, ok := entry.NyxAutoReceivers[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/nyx_auto_registry.go".to_owned(), auto_registry), ], entry_subpath: Some("entry/entry.go".to_owned()), } } /// Generate an `entry/nyx_auto_registry.go` source that publishes a /// `NyxAutoReceivers` map keyed by the target class name to a /// zero-constructed instance. The generated file lives in package /// `entry` so it can reference `class` by bare identifier without /// re-exporting through the harness package. Compile-time enforcement /// of the contract is delegated to the Go compiler — if the entry /// package does not declare `class`, the build fails with a clear /// `undefined: ` error. fn generate_auto_receiver_registry(class: &str) -> String { format!( r##"// Code generated by Nyx — DO NOT EDIT. package entry // NyxAutoReceivers maps a class name to a zero-constructed instance // the dynamic harness uses to reflect on methods at runtime. var NyxAutoReceivers = map[string]interface{{}}{{ "{class}": {class}{{}}, }} "##, class = class, ) } /// Phase 20 (Track M.2) — message-handler harness for Go. /// /// The entry package is expected to declare a top-level handler /// function named `spec.entry_name` taking either a `*entry.NyxPubsubMessage` /// / `*entry.NyxNatsMsg` envelope or a `string` payload. The harness /// mounts the broker loopback declared by [`broker_pubsub`] / /// [`broker_nats`], subscribes the handler reflectively, and publishes /// the payload. Broker pick is derived from /// `spec.framework.adapter`: `pubsub-go` → Pub/Sub, `nats-go` → NATS, /// default → Pub/Sub. fn emit_message_handler_harness(spec: &HarnessSpec, queue: &str) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let handler = &spec.entry_name; let broker = go_broker_for_adapter(spec); let (broker_src, publish_marker, dispatch) = match broker { GoBroker::Nats => ( crate::dynamic::stubs::nats_source(crate::symbol::Lang::Go), crate::dynamic::stubs::NATS_PUBLISH_MARKER, format!( r##" broker := NewNyxNatsLoopback() broker.Subscribe("{queue}", func(msg *NyxNatsMsg) {{ nyxDispatch(msg) }}) fmt.Println("{publish_marker} " + "{queue}") broker.Publish("{queue}", payload)"##, queue = queue, publish_marker = crate::dynamic::stubs::NATS_PUBLISH_MARKER, ), ), GoBroker::Pubsub => ( crate::dynamic::stubs::pubsub_source(crate::symbol::Lang::Go), crate::dynamic::stubs::PUBSUB_PUBLISH_MARKER, format!( r##" broker := NewNyxPubsubLoopback() broker.Subscribe("{queue}", func(msg *NyxPubsubMessage) {{ nyxDispatch(msg) }}) fmt.Println("{publish_marker} " + "{queue}") broker.Publish("{queue}", payload)"##, queue = queue, publish_marker = crate::dynamic::stubs::PUBSUB_PUBLISH_MARKER, ), ), }; // The handler is looked up reflectively through a per-package // `NyxHandlers` registry the entry file publishes (mirrors the // Phase 19 `NyxReceivers` contract). A fallback path probes a few // common exported names so a fixture without the registry still // wires up. let dispatch_inner = format!( r##"func nyxDispatch(msg interface{{}}) {{ defer func() {{ if r := recover(); r != nil {{ fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r) }} }}() fmt.Println("__NYX_SINK_HIT__") cb, ok := entry.NyxHandlers["{handler}"] if !ok {{ fmt.Fprintln(os.Stderr, "NYX_HANDLER_NOT_FOUND: " + "{handler}") os.Exit(78) }} v := reflect.ValueOf(cb) args := make([]reflect.Value, v.Type().NumIn()) for i := 0; i < v.Type().NumIn(); i++ {{ want := v.Type().In(i) got := reflect.ValueOf(msg) if got.Type().AssignableTo(want) {{ args[i] = got }} else if want.Kind() == reflect.String {{ args[i] = reflect.ValueOf(os.Getenv("NYX_PAYLOAD")) }} else {{ args[i] = reflect.Zero(want) }} }} v.Call(args) }} "##, handler = handler, ); let source = format!( r##"// Nyx dynamic harness — message handler (Phase 20 / Track M.2). package main import ( "fmt" "os" "reflect" "nyx-harness/entry" ) {shim} {broker_src} {dispatch_inner} func nyxPayload() string {{ if v := os.Getenv("NYX_PAYLOAD"); v != "" {{ return v }} return "" }} func main() {{ __nyx_install_crash_guard("{handler}") payload := nyxPayload() {dispatch} }} "##, broker_src = broker_src, dispatch_inner = dispatch_inner, dispatch = dispatch, handler = handler, ); let _ = publish_marker; 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()), } } // ── Phase 21 (Track M.3) — synthetic entry-kind harnesses ───────────────────── /// Phase 21 (Track M.3) — GraphQL resolver harness for Go (gqlgen). /// /// Looks up the named resolver via the entry package's `NyxResolvers` /// map (mirrors the `NyxReceivers` / `NyxHandlers` contracts from /// Phase 19 / 20), constructs a synthetic `context.Background()`, and /// invokes the resolver with the payload positionally. fn emit_graphql_resolver_harness(handler: &str, type_name: &str, field: &str) -> HarnessSource { let shim = probe_shim(); let go_mod = generate_go_mod(); let source = format!( r##"// Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3). package main import ( "context" "fmt" "os" "reflect" "nyx-harness/entry" ) {shim} func nyxPayload() string {{ if v := os.Getenv("NYX_PAYLOAD"); v != "" {{ return v }} return "" }} func main() {{ __nyx_install_crash_guard("{type_name}.{field}") payload := nyxPayload() fmt.Println("__NYX_GRAPHQL_RESOLVER__: " + "{type_name}" + "." + "{field}") fmt.Println("__NYX_SINK_HIT__") cb, ok := entry.NyxResolvers["{handler}"] if !ok {{ fmt.Fprintln(os.Stderr, "NYX_RESOLVER_NOT_FOUND: " + "{handler}") os.Exit(78) }} v := reflect.ValueOf(cb) args := make([]reflect.Value, v.Type().NumIn()) for i := 0; i < v.Type().NumIn(); i++ {{ want := v.Type().In(i) if want.Kind() == reflect.String {{ args[i] = reflect.ValueOf(payload) }} else if want.String() == "context.Context" {{ args[i] = reflect.ValueOf(context.Background()) }} else {{ args[i] = reflect.Zero(want) }} }} defer func() {{ if r := recover(); r != nil {{ fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r) }} }}() out := v.Call(args) if len(out) > 0 {{ fmt.Println(out[0].Interface()) }} }} "##, handler = handler, type_name = type_name, field = field, ); 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()), } } #[derive(Debug, Clone, Copy)] enum GoBroker { Pubsub, Nats, } fn go_broker_for_adapter(spec: &HarnessSpec) -> GoBroker { let adapter = spec .framework .as_ref() .map(|b| b.adapter.as_str()) .unwrap_or(""); match adapter { "nats-go" => GoBroker::Nats, _ => GoBroker::Pubsub, } } /// 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, /// Context.String, NewContext) to support the per-shape harness call. fn gin_stub_pkg() -> String { r#"// Phase 15 — minimal gin stub for harness build (not the real gin). package gin import ( "fmt" "io" "net/http" ) type Context struct { Writer http.ResponseWriter Request *http.Request } func NewContext(w http.ResponseWriter, r *http.Request) *Context { return &Context{Writer: w, Request: r} } func (c *Context) Query(name string) string { if c.Request == nil { return "" } return c.Request.URL.Query().Get(name) } func (c *Context) PostForm(name string) string { if c.Request == nil { return "" } _ = c.Request.ParseForm() return c.Request.PostFormValue(name) } func (c *Context) GetRawData() ([]byte, error) { if c.Request == nil || c.Request.Body == nil { return []byte{}, nil } return io.ReadAll(c.Request.Body) } func (c *Context) JSON(code int, obj interface{}) { if c.Writer != nil { c.Writer.WriteHeader(code) fmt.Fprintf(c.Writer, "%v", obj) } } func (c *Context) String(code int, format string, values ...interface{}) { if c.Writer != nil { c.Writer.WriteHeader(code) fmt.Fprintf(c.Writer, format, values...) } } func (c *Context) Redirect(code int, location string) { if c.Writer != nil { c.Writer.Header().Set("Location", location) c.Writer.WriteHeader(code) } } "# .to_owned() } /// 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, EntryKindTag, 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, stubs_required: vec![], framework: None, java_toolchain: crate::dynamic::spec::JavaToolchain::default(), } } #[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_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(&EntryKindTag::Function) ); assert!( GoEmitter .entry_kinds_supported() .contains(&EntryKindTag::HttpRoute) ); assert!( GoEmitter .entry_kinds_supported() .contains(&EntryKindTag::CliSubcommand) ); } #[test] fn entry_kind_hint_names_attempted_and_phase() { let hint = GoEmitter.entry_kind_hint(EntryKindTag::LibraryApi); assert!(hint.contains("LibraryApi")); 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")); } // ── Phase 15: shape detection ──────────────────────────────────────────── fn make_spec_with(kind: EntryKind, name: &str, entry_file: &str) -> HarnessSpec { let mut s = make_spec(PayloadSlot::Param(0)); s.entry_kind = kind; s.entry_name = name.to_owned(); s.entry_file = entry_file.to_owned(); s } #[test] fn shape_detect_http_handler_func() { let src = "package entry\nimport \"net/http\"\nfunc Handle(w http.ResponseWriter, r *http.Request) {}"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::HttpHandlerFunc); } #[test] fn shape_detect_gin_handler() { let src = "package entry\nimport \"nyx-harness/entry/gin\"\nfunc Handle(c *gin.Context) {}"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::GinHandler); } #[test] fn shape_detect_gin_route() { let src = "package main\nimport \"github.com/gin-gonic/gin\"\nfunc Handle(c *gin.Context) {}"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::GinRoute); } #[test] fn shape_detect_echo_route() { let src = "package main\nimport \"github.com/labstack/echo/v4\"\nfunc Handle(c echo.Context) error { return nil }"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::EchoRoute); } #[test] fn shape_detect_fiber_route() { let src = "package main\nimport \"github.com/gofiber/fiber/v2\"\nfunc Handle(c *fiber.Ctx) error { return nil }"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::FiberRoute); } #[test] fn shape_detect_chi_route() { let src = "package main\nimport \"github.com/go-chi/chi/v5\"\nfunc Handle(w http.ResponseWriter, r *http.Request) {}"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::ChiRoute); } #[test] fn gin_route_emits_marker_in_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::GinRoute); assert!( src.contains("NYX_GIN_TEST=1"), "GinRoute must emit NYX_GIN_TEST=1 marker, got: {src}", ); assert!(src.contains("httptest.NewRequest")); } #[test] fn echo_route_emits_marker_in_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::EchoRoute); assert!(src.contains("NYX_ECHO_TEST=1")); } #[test] fn fiber_route_emits_marker_in_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::FiberRoute); assert!(src.contains("NYX_FIBER_TEST=1")); } #[test] fn chi_route_emits_marker_in_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::ChiRoute); assert!(src.contains("NYX_CHI_TEST=1")); } #[test] fn shape_detect_flag_parse_cli() { let src = "package entry\nimport \"flag\"\nfunc Run() { flag.Parse() }"; let spec = make_spec_with(EntryKind::CliSubcommand, "Run", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::FlagParseCli); } #[test] fn shape_detect_fuzz_variadic() { let src = "package entry\nfunc FuzzHandle(data []byte) error { return nil }"; let spec = make_spec_with(EntryKind::Function, "FuzzHandle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::FuzzVariadic); } #[test] fn shape_detect_generic_fallback() { let src = "package entry\nfunc Login(payload string) {}"; let spec = make_spec_with(EntryKind::Function, "Login", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::Generic); } #[test] fn http_shape_emits_httptest_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::HttpHandlerFunc); assert!(src.contains("httptest.NewRequest")); assert!(src.contains("httptest.NewRecorder")); assert!(src.contains("entry.Handle(rw, req)")); } #[test] fn gin_shape_emits_context_invocation() { let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); let src = generate_main_go(&spec, GoShape::GinHandler); assert!(src.contains("gin.NewContext")); assert!(src.contains("entry.Handle(ctx)")); } #[test] fn cli_shape_emits_os_args_setup() { let mut spec = make_spec_with(EntryKind::CliSubcommand, "Run", "entry.go"); spec.payload_slot = PayloadSlot::Argv(0); let src = generate_main_go(&spec, GoShape::FlagParseCli); assert!(src.contains("os.Args = []string")); assert!(src.contains("entry.Run()")); } #[test] fn fuzz_shape_emits_bytes_invocation() { let spec = make_spec_with(EntryKind::Function, "FuzzHandle", "entry.go"); let src = generate_main_go(&spec, GoShape::FuzzVariadic); assert!(src.contains("entry.FuzzHandle([]byte(payload))")); } #[test] fn emit_splices_probe_shim_and_installs_crash_guard() { let spec = make_spec(PayloadSlot::Param(0)); let h = emit(&spec).unwrap(); assert!( h.source.contains("__nyx_probe shim (Phase 06 — Track C.1"), "probe_shim banner missing from generated main.go — splicing regressed", ); assert!( h.source.contains("func __nyx_install_crash_guard("), "install_crash_guard definition missing from generated main.go", ); assert!( h.source .contains("__nyx_install_crash_guard(\"HandleRequest\")"), "install_crash_guard call site missing or wrong callee in main()", ); let install_pos = h .source .find("__nyx_install_crash_guard(\"HandleRequest\")") .unwrap(); let payload_pos = h.source.find("payload := nyxPayload()").unwrap(); let invoke_pos = h.source.find("entry.HandleRequest(payload)").unwrap(); assert!( payload_pos < install_pos && install_pos < invoke_pos, "install_crash_guard ordering wrong: payload_pos={payload_pos} install_pos={install_pos} invoke_pos={invoke_pos}", ); } #[test] fn emit_includes_shim_imports_in_import_block() { let spec = make_spec(PayloadSlot::Param(0)); let h = emit(&spec).unwrap(); for path in SHIM_IMPORTS { let quoted = format!("\"{path}\""); assert!( h.source.contains("ed), "expected shim-required import {quoted} in generated main.go", ); } } #[test] fn probe_shim_publishes_stub_http_recorder() { let shim = probe_shim(); assert!( shim.contains("func __nyx_stub_http_record"), "Go probe shim must define __nyx_stub_http_record" ); assert!( shim.contains("NYX_HTTP_LOG"), "stub recorder must read NYX_HTTP_LOG" ); } #[test] fn probe_shim_publishes_stub_sql_recorder() { let shim = probe_shim(); assert!( shim.contains("func __nyx_stub_sql_record"), "Go probe shim must define __nyx_stub_sql_record" ); assert!( shim.contains("NYX_SQL_LOG"), "stub recorder must read NYX_SQL_LOG" ); assert!( shim.contains("strings.HasSuffix(query, \"\\n\")"), "Go SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record" ); } #[test] fn chain_step_splices_probe_shim_for_composite_reverify() { let step = chain_step(Some(b""), None); assert!( step.source.contains("__nyx_probe"), "Go chain step must splice the probe shim" ); assert!( step.source.starts_with("package main"), "Go chain step must open with package main" ); assert!( step.source.contains("os.Getenv(\"NYX_PREV_OUTPUT\")"), "Go chain step must keep its NYX_PREV_OUTPUT forwarder" ); let import_close = step.source.find(")\n").expect("import block must close"); let shim_pos = step.source.find("__nyx_probe").unwrap(); let main_pos = step.source.find("func main()").unwrap(); assert!( import_close < shim_pos, "probe shim must come after the import block", ); assert!( shim_pos < main_pos, "probe shim must come before func main() so its helpers are in scope when a sink rewrite splices in", ); for path in SHIM_IMPORTS { let quoted = format!("\"{path}\""); assert!( step.source.contains("ed), "Go chain step must merge shim-required import {quoted} into its import block", ); } // Driver imports preserved alongside the shim imports. assert!(step.source.contains("\"fmt\"")); assert!(step.source.contains("\"os\"")); } // ── Phase 08 / 09 tier-(a) helpers + emitters ─────────────────────────── #[test] fn rewrite_package_replaces_first_package_line() { let src = "// header\npackage vuln\n\nimport \"net/http\"\n\nfunc Run() {}\n"; let out = rewrite_package(src, "vulnentry"); assert!( out.contains("\npackage vulnentry\n"), "rewrite must produce `package vulnentry`, got:\n{out}", ); assert!( !out.contains("\npackage vuln\n"), "original `package vuln` must be gone after rewrite, got:\n{out}", ); // Other lines preserved verbatim. assert!(out.contains("// header")); assert!(out.contains("import \"net/http\"")); assert!(out.contains("func Run() {}")); } #[test] fn rewrite_package_handles_crlf_line_endings() { let src = "package benign\r\nimport \"net/http\"\r\n"; let out = rewrite_package(src, "vulnentry"); assert!(out.starts_with("package vulnentry\r\n")); assert!(out.contains("import \"net/http\"")); } #[test] fn rewrite_package_passes_through_when_no_package_line() { let src = "// no package decl here\nimport \"net/http\"\n"; let out = rewrite_package(src, "vulnentry"); assert_eq!(out, src); } #[test] fn header_injection_tier_a_fires_when_net_http_imported() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::HEADER_INJECTION; spec.entry_file = "tests/dynamic_fixtures/header_injection/go/vuln.go".into(); let harness = emit_header_injection_harness(&spec); assert!( harness.source.contains("nyx-harness/internal/vulnentry"), "tier-(a) header_injection must import the rewritten fixture package", ); assert!( harness.source.contains("nyxHeaderViaFixture(payload)"), "tier-(a) header_injection must dispatch via fixture wrapper", ); assert!( harness.source.contains("vulnentry.Run(rec, payload)"), "tier-(a) header_injection must call .Run(rec, payload)", ); assert!( harness.source.contains("rec.Header()"), "tier-(a) header_injection must walk rec.Header() for captured headers", ); // Rewritten fixture must be staged under internal/vulnentry/. let staged = harness .extra_files .iter() .find(|(p, _)| p == "internal/vulnentry/vulnentry.go"); assert!( staged.is_some(), "tier-(a) header_injection must stage internal/vulnentry/vulnentry.go", ); assert!( staged.unwrap().1.contains("package vulnentry"), "staged fixture must carry the rewritten package declaration", ); } #[test] fn header_injection_tier_b_falls_back_when_no_net_http() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::HEADER_INJECTION; spec.entry_file = "/nonexistent/missing.go".into(); let harness = emit_header_injection_harness(&spec); assert!( !harness.source.contains("nyx-harness/internal/vulnentry"), "tier-(b) header_injection must not import a fixture package", ); assert!( harness.source.contains("nyxHeaderProbe(\"Set-Cookie\", payload)"), "tier-(b) header_injection must emit synthetic Set-Cookie probe", ); assert!( harness .extra_files .iter() .all(|(p, _)| p != "internal/vulnentry/vulnentry.go"), "tier-(b) header_injection must not stage a rewritten fixture", ); } #[test] fn open_redirect_tier_a_fires_when_gin_imported() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = "tests/dynamic_fixtures/open_redirect/go/vuln.go".into(); let harness = emit_open_redirect_harness(&spec); assert!( harness.source.contains("nyx-harness/internal/vulnentry"), "tier-(a) open_redirect must import the rewritten fixture package", ); assert!( harness.source.contains("nyx-harness/internal/vulnentry/gin"), "tier-(a) open_redirect must import the local gin stub", ); assert!( harness.source.contains("nyxRedirectViaFixture(payload)"), "tier-(a) open_redirect must dispatch via fixture wrapper", ); assert!( harness.source.contains("vulnentry.Run(ctx, payload)"), "tier-(a) open_redirect must call .Run(ctx, payload)", ); assert!( harness.source.contains("rec.Header().Get(\"Location\")"), "tier-(a) open_redirect must read Location off the recorder", ); let staged_fixture = harness .extra_files .iter() .find(|(p, _)| p == "internal/vulnentry/vulnentry.go"); assert!( staged_fixture.is_some(), "tier-(a) open_redirect must stage internal/vulnentry/vulnentry.go", ); let staged_fixture = staged_fixture.unwrap(); assert!( staged_fixture.1.contains("package vulnentry"), "staged fixture must carry the rewritten package", ); assert!( staged_fixture .1 .contains("\"nyx-harness/internal/vulnentry/gin\""), "staged fixture must have its gin import rewritten to the local stub", ); let staged_gin = harness .extra_files .iter() .find(|(p, _)| p == "internal/vulnentry/gin/gin.go"); assert!( staged_gin.is_some(), "tier-(a) open_redirect must stage the gin stub", ); assert!( staged_gin.unwrap().1.contains("func (c *Context) Redirect("), "staged gin stub must expose Redirect", ); } #[test] fn open_redirect_tier_b_falls_back_when_no_framework() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = "/nonexistent/missing.go".into(); let harness = emit_open_redirect_harness(&spec); assert!( !harness.source.contains("nyx-harness/internal/vulnentry"), "tier-(b) open_redirect must not import a fixture package", ); assert!( harness .source .contains("nyxRedirectProbe(payload, requestHost)"), "tier-(b) open_redirect must emit synthetic redirect probe", ); assert!( harness .extra_files .iter() .all(|(p, _)| !p.starts_with("internal/vulnentry/")), "tier-(b) open_redirect must not stage any rewritten fixture or stub", ); } #[test] fn emit_open_redirect_harness_ships_follow_location_helper() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = "/nonexistent/missing.go".into(); let harness = emit_open_redirect_harness(&spec); assert!( harness.source.contains("func nyxFollowLocation(location string)"), "OPEN_REDIRECT harness must declare the nyxFollowLocation helper", ); assert!( harness.source.contains("strings.HasPrefix(location, \"http://127.0.0.1\")"), "follower must gate on loopback 127.0.0.1 host prefix", ); assert!( harness.source.contains("strings.HasPrefix(location, \"http://localhost\")"), "follower must gate on loopback localhost host prefix", ); assert!( harness.source.contains("strings.HasPrefix(location, \"http://host-gateway\")"), "follower must gate on loopback host-gateway prefix", ); assert!( harness.source.contains("client.Get(location)"), "follower must drive a real http.Client.Get against the captured Location", ); // Tier-(b) callsite must call the follower on the synthetic payload. assert!( harness .source .contains("nyxRedirectProbe(payload, requestHost)\n\tnyxFollowLocation(payload)"), "tier-(b) callsite must invoke nyxFollowLocation after the synthetic probe", ); // Even tier-(b) must pull in net/http so the follower compiles. assert!( harness.source.contains("\"net/http\""), "OPEN_REDIRECT harness must always import net/http so nyxFollowLocation compiles", ); } #[test] fn emit_open_redirect_harness_follows_captured_location_in_tier_a() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_name = "Run".into(); spec.expected_cap = Cap::OPEN_REDIRECT; spec.entry_file = "tests/dynamic_fixtures/open_redirect/go/vuln.go".into(); let harness = emit_open_redirect_harness(&spec); // Tier-(a) gin: when fixture call succeeds, follow the captured loc. assert!( harness .source .contains("nyxRedirectProbe(loc, requestHost)\n\t\tnyxFollowLocation(loc)"), "tier-(a) callsite must invoke nyxFollowLocation on the captured Location", ); // Tier-(a) fixture-call-failed branch falls back to payload-as-loc. assert!( harness .source .contains("nyxRedirectProbe(payload, requestHost)\n\t\tnyxFollowLocation(payload)"), "tier-(a) fixture-failure branch must still follow the synthetic payload", ); } #[test] fn gin_stub_pkg_exposes_redirect_method() { let stub = gin_stub_pkg(); assert!( stub.contains("func (c *Context) Redirect(code int, location string)"), "gin stub must expose a Redirect method tier-(a) open_redirect drives the fixture through", ); // The Redirect method must set Location and write the status. assert!(stub.contains("c.Writer.Header().Set(\"Location\", location)")); assert!(stub.contains("c.Writer.WriteHeader(code)")); } fn make_crypto_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::CRYPTO; spec.entry_file = entry_file.to_owned(); spec.entry_name = entry_name.to_owned(); spec } #[test] fn emit_dispatches_to_crypto_harness_when_cap_is_crypto() { let h = emit(&make_crypto_spec( "tests/dynamic_fixtures/crypto/go/vuln.go", "Run", )) .unwrap(); assert!( h.source.contains("nyxWeakKeyProbe"), "dispatcher must short-circuit Cap::CRYPTO into emit_crypto_harness so the weak-key probe shim is present", ); assert!( h.source.contains("\"kind\": \"WeakKey\""), "crypto harness must record probes with `kind: WeakKey` so the WeakKeyEntropy predicate fires", ); } #[test] fn emit_crypto_harness_routes_through_internal_vulnentry_package() { let h = emit_crypto_harness(&make_crypto_spec( "tests/dynamic_fixtures/crypto/go/vuln.go", "Run", )); let staged = h .extra_files .iter() .find(|(name, _)| name == "internal/vulnentry/vulnentry.go"); assert!( staged.is_some(), "tier-(a) crypto harness must stage the fixture under internal/vulnentry/ so main.go can import it", ); let body = &staged.unwrap().1; assert!( body.contains("package vulnentry"), "fixture package name must be rewritten to vulnentry so the import path resolves", ); assert!( h.source.contains("nyx-harness/internal/vulnentry"), "main.go must import the rewritten vulnentry package", ); assert!( h.source.contains("vulnentry.Run(payload)"), "main.go must invoke the entry function on the rewritten fixture, not a synthetic stub", ); } #[test] fn emit_crypto_harness_emits_weak_key_probe_kind() { let h = emit_crypto_harness(&make_crypto_spec( "tests/dynamic_fixtures/crypto/go/vuln.go", "Run", )); assert!( h.source.contains("\"kind\": \"WeakKey\", \"key_int\":"), "Go CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires", ); assert!( h.source.contains("__NYX_SINK_HIT__"), "Go CRYPTO harness must print the universal sink-hit sentinel", ); } #[test] fn emit_crypto_harness_reduces_byte_slice_returns_via_big_endian() { let h = emit_crypto_harness(&make_crypto_spec( "tests/dynamic_fixtures/crypto/go/benign.go", "Run", )); assert!( h.source.contains("binary.BigEndian.Uint64"), "Go CRYPTO harness must use binary.BigEndian.Uint64 so byte-slice returns reduce to a magnitude that exceeds the 16-bit budget on CSPRNG keys", ); assert!( h.source.contains("reflect.ValueOf"), "Go CRYPTO harness must use reflect to dispatch on the produced key's type", ); assert!( h.source.contains("case reflect.Slice"), "Go CRYPTO harness must handle the []byte branch from CSPRNG benign controls", ); } #[test] fn emit_crypto_harness_falls_back_when_fixture_source_unavailable() { let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::CRYPTO; spec.entry_file = "/nonexistent/path/missing.go".into(); spec.entry_name = "Run".into(); let h = emit_crypto_harness(&spec); let staged = h .extra_files .iter() .find(|(name, _)| name == "internal/vulnentry/vulnentry.go"); assert!( staged.is_none(), "fallback path must not stage a vulnentry copy when the fixture cannot be read", ); assert!( !h.source.contains("nyx-harness/internal/vulnentry"), "fallback path must not import the missing vulnentry package", ); assert!( h.source.contains("nyxWeakKeyProbe"), "fallback path must still emit a weak-key probe so the universal sink-hit path fires", ); } }