nyx/src/dynamic/lang/go.rs

2597 lines
92 KiB
Rust

//! 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<HarnessSource, UnsupportedReason> {
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::<Vec<_>>()
.join(".");
let go_version = if go_version.is_empty() {
"1.22".to_owned()
} else {
go_version
};
let mut deps: Vec<String> = Vec::new();
let mut seen: std::collections::HashSet<String> = 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 = "<redacted-by-nyx-policy>"
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<HarnessSource, UnsupportedReason> {
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 `<!ENTITY>` 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.<EntryFn>(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, "&", "&amp;")
escaped = strings.ReplaceAll(escaped, "\"", "&quot;")
escaped = strings.ReplaceAll(escaped, "<", "&lt;")
return "<?xml version=\"1.0\"?>\n<!DOCTYPE data [\n <!ENTITY xxe SYSTEM \"" + escaped + "\">\n]>\n<data>&xxe;</data>"
}}
return payload
}}
func nyxXmlParse(payload string) bool {{
// Real parser hook: walk Go's encoding/xml.Decoder token stream.
// The decoder parses <!DOCTYPE name [<!ENTITY x SYSTEM "uri">]>
// 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 <Name>(w http.ResponseWriter, value string)`, the harness
/// rewrites the fixture's `package <X>` 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 <ident>$` line in `src` to
/// `package <target>`. 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 <X>` 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.<Run>(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.<Run>(httptest.NewRecorder(), <req>, 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::<Vec<_>>()
.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.<EntryFn>(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: <class>` 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::<String>() + 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(&quoted),
"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"<prev>"), 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(&quoted),
"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 <entry>.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 <entry>.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",
);
}
}