mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss] phase 14: Track B — Java harness emitter shapes
This commit is contained in:
parent
7628c48930
commit
bd1bd0ce84
36 changed files with 1793 additions and 155 deletions
|
|
@ -534,7 +534,12 @@ fn compute_go_source_hash(workdir: &Path) -> String {
|
|||
|
||||
/// Prepare compiled Java classes for `spec`.
|
||||
///
|
||||
/// Runs `javac NyxHarness.java Entry.java` in `workdir`.
|
||||
/// Runs `javac` over every `*.java` file in `workdir` (recursive). Phase 14
|
||||
/// shape-aware fixtures may stage additional source files alongside the
|
||||
/// generated `NyxHarness.java` (annotation stubs, servlet-request stubs,
|
||||
/// helper classes); the compiler must see all of them in a single
|
||||
/// invocation so the inter-class references resolve.
|
||||
///
|
||||
/// Class files land in the workdir (default package, no output dir).
|
||||
///
|
||||
/// Build isolation is NOT yet implemented (deferred). `javac` runs on the host.
|
||||
|
|
@ -544,11 +549,14 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
|
|||
let source_hash = compute_java_source_hash(workdir);
|
||||
let cache_path = build_cache_path(&source_hash, "java", &spec.toolchain_id)?;
|
||||
|
||||
// Cache hit: class files already compiled. Restore them to workdir so the
|
||||
// classpath (which points to workdir, not cache_path) can find them when a
|
||||
// different finding hits the same compiled artefact via a fresh spec_hash.
|
||||
let cached_classes = collect_class_files(&cache_path);
|
||||
|
||||
// Cache hit: at least the harness class is compiled. Restore every
|
||||
// cached `.class` to workdir so the classpath (which points to
|
||||
// workdir, not cache_path) can find them when a different finding
|
||||
// hits the same compiled artefact via a fresh spec_hash.
|
||||
if cache_path.join("NyxHarness.class").exists() {
|
||||
for cls in &["NyxHarness.class", "Entry.class"] {
|
||||
for cls in &cached_classes {
|
||||
let src = cache_path.join(cls);
|
||||
let dst = workdir.join(cls);
|
||||
if src.exists() && !dst.exists() {
|
||||
|
|
@ -581,8 +589,20 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
|
|||
}
|
||||
Err(e) => {
|
||||
last_err = e;
|
||||
let _ = std::fs::remove_file(cache_path.join("NyxHarness.class"));
|
||||
let _ = std::fs::remove_file(cache_path.join("Entry.class"));
|
||||
// Best-effort clean-up: drop every cached `.class` so the
|
||||
// next attempt re-compiles from source.
|
||||
if let Ok(entries) = std::fs::read_dir(&cache_path) {
|
||||
for entry in entries.flatten() {
|
||||
if entry
|
||||
.path()
|
||||
.extension()
|
||||
.map(|e| e == "class")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = std::fs::remove_file(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -593,13 +613,15 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
|
|||
fn try_compile_java(workdir: &Path, cache_path: &Path) -> Result<(), String> {
|
||||
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
|
||||
|
||||
let sources = collect_java_sources(workdir);
|
||||
if sources.is_empty() {
|
||||
return Err("no Java sources found in workdir".to_owned());
|
||||
}
|
||||
|
||||
// Compile sources — class files are written to workdir by default.
|
||||
let mut args = vec!["-d".to_owned(), workdir.to_string_lossy().into_owned()];
|
||||
for src in &["NyxHarness.java", "Entry.java"] {
|
||||
let p = workdir.join(src);
|
||||
if p.exists() {
|
||||
args.push(p.to_string_lossy().into_owned());
|
||||
}
|
||||
for src in &sources {
|
||||
args.push(src.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
let output = Command::new(&javac)
|
||||
|
|
@ -615,21 +637,74 @@ fn try_compile_java(workdir: &Path, cache_path: &Path) -> Result<(), String> {
|
|||
return Err(String::from_utf8_lossy(&output.stderr).into_owned());
|
||||
}
|
||||
|
||||
// Copy class files to cache.
|
||||
for cls in &["NyxHarness.class", "Entry.class"] {
|
||||
let src = workdir.join(cls);
|
||||
// Copy class files to cache. `javac -d workdir` writes nested
|
||||
// package directories under workdir; preserve the relative layout
|
||||
// when caching so the restore path can recreate them.
|
||||
for cls in collect_class_files(workdir) {
|
||||
let src = workdir.join(&cls);
|
||||
let dst = cache_path.join(&cls);
|
||||
if let Some(parent) = dst.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if src.exists() {
|
||||
let _ = std::fs::copy(&src, cache_path.join(cls));
|
||||
let _ = std::fs::copy(&src, &dst);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively enumerate every `*.java` source file under `workdir`.
|
||||
fn collect_java_sources(workdir: &Path) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![workdir.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if path.extension().map(|e| e == "java").unwrap_or(false) {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// Recursively enumerate every `*.class` file relative to `root`.
|
||||
fn collect_class_files(root: &Path) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![root.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if path.extension().map(|e| e == "class").unwrap_or(false) {
|
||||
if let Ok(rel) = path.strip_prefix(root) {
|
||||
out.push(rel.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
fn compute_java_source_hash(workdir: &Path) -> String {
|
||||
let mut h = Hasher::new();
|
||||
for fname in &["NyxHarness.java", "Entry.java"] {
|
||||
if let Ok(content) = std::fs::read(workdir.join(fname)) {
|
||||
h.update(fname.as_bytes());
|
||||
for path in collect_java_sources(workdir) {
|
||||
if let Ok(content) = std::fs::read(&path) {
|
||||
let rel = path.strip_prefix(workdir).unwrap_or(&path);
|
||||
h.update(rel.to_string_lossy().as_bytes());
|
||||
h.update(&content);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,37 @@
|
|||
//! Java harness emitter.
|
||||
//!
|
||||
//! Generates a Java `NyxHarness.java` that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. Calls `Entry.{entry_name}(payload)` from the co-located `Entry.java`.
|
||||
//! 3. Catches all exceptions to prevent harness crashes from masking results.
|
||||
//! Phase 14 (Track B Java vertical) replaces the single legacy `emit`
|
||||
//! body with dispatch over [`JavaShape`] — the cross product of
|
||||
//! [`EntryKind`] and a lightweight per-file shape detector that inspects
|
||||
//! the entry file for servlet / Spring / Quarkus annotations, JUnit
|
||||
//! markers, and `static main(String[])` signatures.
|
||||
//!
|
||||
//! Sink-reachability probe: fixtures explicitly emit `System.out.println("__NYX_SINK_HIT__")`
|
||||
//! before the actual sink call (same pattern as Rust and Go fixtures).
|
||||
//! Each shape emits a single `NyxHarness.java` that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64`.
|
||||
//! 2. Locates the entry class (default-package, derived from the entry
|
||||
//! file basename) and invokes its method via the per-shape adapter.
|
||||
//! 3. Catches all exceptions so the JVM exit shape stays observable.
|
||||
//!
|
||||
//! Build step: `prepare_java()` in `build_sandbox.rs` runs `javac NyxHarness.java Entry.java`
|
||||
//! in the workdir. The compiled `.class` files land in the workdir.
|
||||
//! Sink-reachability probe: fixtures explicitly emit
|
||||
//! `System.out.println("__NYX_SINK_HIT__")` before the actual sink call
|
||||
//! (same pattern as Rust and Go fixtures).
|
||||
//!
|
||||
//! File layout in workdir:
|
||||
//! ```text
|
||||
//! NyxHarness.java ← harness main class (generated)
|
||||
//! Entry.java ← entry class (copied from project)
|
||||
//! NyxHarness.class ← compiled by prepare_java()
|
||||
//! Entry.class ← compiled by prepare_java()
|
||||
//! ```
|
||||
//! Build step: `prepare_java()` in `build_sandbox.rs` runs `javac` over
|
||||
//! every `*.java` file in the workdir. Shape fixtures bundle their own
|
||||
//! annotation / type stubs (e.g. a minimal `HttpServletRequest.java`
|
||||
//! when the shape needs servlet plumbing) so the JDK can compile the
|
||||
//! source without pulling Maven dependencies.
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(0)` — pass payload as `String` first argument.
|
||||
//! - `PayloadSlot::EnvVar(name)` — set system property before calling entry.
|
||||
//! - Other slots produce `UnsupportedReason::PayloadSlotUnsupported`.
|
||||
//! - [`PayloadSlot::Param`] — pass payload as `String` first argument
|
||||
//! (n-th positional for `Param(n)` where `n > 0`).
|
||||
//! - [`PayloadSlot::EnvVar`] — set a system property before invocation.
|
||||
//! - [`PayloadSlot::QueryParam`] / [`PayloadSlot::HttpBody`] — surfaced
|
||||
//! to servlet / Spring / Quarkus adapters as the request body or
|
||||
//! query parameter value.
|
||||
//! - [`PayloadSlot::Argv`] — appended to a `String[] args` for
|
||||
//! `static main` shapes.
|
||||
//! - Other slots produce [`UnsupportedReason::PayloadSlotUnsupported`].
|
||||
//!
|
||||
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
|
|
@ -30,15 +39,22 @@ use crate::dynamic::environment::{Environment, RuntimeArtifacts};
|
|||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Java. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct JavaEmitter;
|
||||
|
||||
/// Entry kinds the Java emitter currently understands. Extended in Phase 14
|
||||
/// (Track B Java vertical) to include `HttpRoute` (servlet / Spring /
|
||||
/// Quarkus) and JUnit static-method shapes.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
/// Entry kinds the Java emitter understands after Phase 14.
|
||||
///
|
||||
/// `HttpRoute` covers servlet / Spring / Quarkus shapes. `CliSubcommand`
|
||||
/// covers `public static void main(String[])`. `Function` covers JUnit
|
||||
/// tests and plain static methods.
|
||||
const SUPPORTED: &[EntryKind] = &[
|
||||
EntryKind::Function,
|
||||
EntryKind::HttpRoute,
|
||||
EntryKind::CliSubcommand,
|
||||
];
|
||||
|
||||
impl LangEmitter for JavaEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
|
|
@ -51,7 +67,7 @@ impl LangEmitter for JavaEmitter {
|
|||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add servlet / Spring / Quarkus shapes in phase 14"
|
||||
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 14 shape dispatch"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -60,75 +76,118 @@ impl LangEmitter for JavaEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 09 — Track D.2: synthesise a minimal `pom.xml` that pins the
|
||||
/// Java toolchain and lists the direct dep top-level packages as
|
||||
/// dependencies. Each direct dep maps to `<groupId>{pkg}</groupId>`
|
||||
/// with an artifact id matching the package name; this is a best-effort
|
||||
/// stub and Phase 10 corpus expansion will introduce a known-good
|
||||
/// group→artifact registry.
|
||||
pub fn materialize_java(env: &Environment) -> RuntimeArtifacts {
|
||||
let mut artifacts = RuntimeArtifacts::new();
|
||||
let java_version = env
|
||||
.toolchain
|
||||
.version_string
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or("21")
|
||||
.to_owned();
|
||||
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_java_stdlib(d) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(d.clone()) {
|
||||
deps.push(d.clone());
|
||||
}
|
||||
}
|
||||
deps.sort_unstable();
|
||||
// ── Phase 14: shape detector ─────────────────────────────────────────────────
|
||||
|
||||
let mut body = String::with_capacity(256);
|
||||
body.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
body.push_str("<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n");
|
||||
body.push_str(" <modelVersion>4.0.0</modelVersion>\n");
|
||||
body.push_str(" <groupId>nyx</groupId>\n");
|
||||
body.push_str(" <artifactId>harness</artifactId>\n");
|
||||
body.push_str(" <version>0.0.1</version>\n");
|
||||
body.push_str(" <properties>\n");
|
||||
body.push_str(&format!(
|
||||
" <maven.compiler.source>{java_version}</maven.compiler.source>\n"
|
||||
));
|
||||
body.push_str(&format!(
|
||||
" <maven.compiler.target>{java_version}</maven.compiler.target>\n"
|
||||
));
|
||||
body.push_str(" </properties>\n");
|
||||
if !deps.is_empty() {
|
||||
body.push_str(" <dependencies>\n");
|
||||
for d in &deps {
|
||||
body.push_str(" <dependency>\n");
|
||||
body.push_str(&format!(" <groupId>{d}</groupId>\n"));
|
||||
body.push_str(&format!(" <artifactId>{d}</artifactId>\n"));
|
||||
body.push_str(" <version>LATEST</version>\n");
|
||||
body.push_str(" </dependency>\n");
|
||||
}
|
||||
body.push_str(" </dependencies>\n");
|
||||
}
|
||||
body.push_str("</project>\n");
|
||||
artifacts.push("pom.xml", body);
|
||||
artifacts
|
||||
/// 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 [`JavaShape::StaticMethod`],
|
||||
/// which preserves the pre-Phase-14 behaviour (direct static method call).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum JavaShape {
|
||||
/// `public class … extends HttpServlet { void doGet(req, resp) }`.
|
||||
/// Harness instantiates the class via the default constructor and
|
||||
/// invokes `doGet` with a minimal `HttpServletRequest` / `Response`
|
||||
/// stub-pair via reflection.
|
||||
ServletDoGet,
|
||||
/// `void doPost(req, resp)` variant. Same adapter shape as doGet
|
||||
/// but uses `POST` semantics for query-vs-body wiring.
|
||||
ServletDoPost,
|
||||
/// Spring `@RestController` / `@Controller` with a `@RequestMapping`
|
||||
/// / `@GetMapping` / `@PostMapping` handler. Harness instantiates
|
||||
/// the controller via reflection (default ctor) and invokes the
|
||||
/// handler method with the payload routed into the matching
|
||||
/// `String` parameter.
|
||||
SpringController,
|
||||
/// `public static void main(String[] args)`. Harness calls
|
||||
/// `Class.forName(name).getMethod("main", String[].class)` and
|
||||
/// passes a one-element argv populated from the payload.
|
||||
StaticMain,
|
||||
/// JUnit 4 (`@Test`) or JUnit 5 (`@Test` from `org.junit.jupiter.api`).
|
||||
/// Harness instantiates the test class and invokes the annotated
|
||||
/// method via reflection — no JUnit runner needed since we drive a
|
||||
/// single test method.
|
||||
JunitTest,
|
||||
/// Quarkus reactive route: `@Path("/foo")` + `@GET`/`@POST` on a
|
||||
/// method. Harness invokes the method via reflection like Spring.
|
||||
QuarkusRoute,
|
||||
/// Plain static method — legacy default behaviour from before
|
||||
/// Phase 14. Harness directly calls `{Class}.{method}(payload)`.
|
||||
StaticMethod,
|
||||
}
|
||||
|
||||
fn is_java_stdlib(name: &str) -> bool {
|
||||
// Best-effort: only `java` / `javax` / `sun` are guaranteed JDK.
|
||||
// `jakarta` ships separately under Jakarta EE so it stays out.
|
||||
// Top-level segments `com` / `org` cover both JDK (`com.sun`) and
|
||||
// third-party (`com.google`, `org.springframework`) — the import
|
||||
// extractor only keeps the first segment, so a richer registry has
|
||||
// to land before we can pin a meaningful Maven artifact from these.
|
||||
// Phase 10 corpus expansion ships that registry.
|
||||
matches!(name, "java" | "javax" | "sun" | "com" | "org" | "jakarta")
|
||||
impl JavaShape {
|
||||
/// Detect the shape from `(spec, source)`. `source` is the literal
|
||||
/// bytes of the entry file (best-effort — if it could not be read,
|
||||
/// pass an empty string and the function returns
|
||||
/// [`Self::StaticMethod`]).
|
||||
///
|
||||
/// Framework / annotation detection wins over the [`EntryKind`]
|
||||
/// axis: when the source clearly imports a servlet or Spring
|
||||
/// controller the shape is selected even if the spec derivation
|
||||
/// pipeline tagged the entry kind as [`EntryKind::Function`].
|
||||
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
|
||||
let entry = spec.entry_name.as_str();
|
||||
let kind = spec.entry_kind;
|
||||
|
||||
let has_servlet = source.contains("HttpServlet")
|
||||
|| source.contains("javax.servlet")
|
||||
|| source.contains("jakarta.servlet");
|
||||
let has_spring_controller = source.contains("@RestController")
|
||||
|| source.contains("@Controller")
|
||||
|| source.contains("@RequestMapping")
|
||||
|| source.contains("@GetMapping")
|
||||
|| source.contains("@PostMapping");
|
||||
let has_quarkus = source.contains("@Path(")
|
||||
|| source.contains("io.quarkus")
|
||||
|| source.contains("jakarta.ws.rs");
|
||||
let has_junit = source.contains("@Test")
|
||||
&& (source.contains("org.junit") || source.contains("junit.framework"));
|
||||
let has_main = entry == "main" || source.contains("static void main(");
|
||||
|
||||
// Servlet beats Spring when both fire (e.g. a Spring app that
|
||||
// mounts a raw servlet) — the doGet/doPost signature is more
|
||||
// specific.
|
||||
if has_servlet {
|
||||
if entry == "doPost" || source.contains("void doPost(") {
|
||||
return Self::ServletDoPost;
|
||||
}
|
||||
if entry == "doGet" || source.contains("void doGet(") {
|
||||
return Self::ServletDoGet;
|
||||
}
|
||||
return Self::ServletDoGet;
|
||||
}
|
||||
if has_quarkus {
|
||||
return Self::QuarkusRoute;
|
||||
}
|
||||
if has_spring_controller {
|
||||
return Self::SpringController;
|
||||
}
|
||||
if has_main {
|
||||
return Self::StaticMain;
|
||||
}
|
||||
if has_junit {
|
||||
return Self::JunitTest;
|
||||
}
|
||||
|
||||
if kind == EntryKind::CliSubcommand {
|
||||
return Self::StaticMain;
|
||||
}
|
||||
if kind == EntryKind::HttpRoute {
|
||||
return Self::SpringController;
|
||||
}
|
||||
Self::StaticMethod
|
||||
}
|
||||
}
|
||||
|
||||
// (Helper retired in Phase 14 — the shape detector now uses direct
|
||||
// `source.contains` matches against the method-signature head because
|
||||
// the JDK accepts whitespace / newline / modifier variation that no
|
||||
// single template captures.)
|
||||
|
||||
|
||||
// ── Probe shim (Phase 06 + Phase 08) ─────────────────────────────────────────
|
||||
|
||||
/// Source of the `__nyx_probe` shim for the Java harness (Phase 06 —
|
||||
/// Track C.1).
|
||||
///
|
||||
|
|
@ -271,21 +330,104 @@ pub fn probe_shim() -> &'static str {
|
|||
"#
|
||||
}
|
||||
|
||||
// ── Runtime / pom.xml synthesis (Phase 09) ──────────────────────────────────
|
||||
|
||||
/// Phase 09 — Track D.2: synthesise a minimal `pom.xml` that pins the
|
||||
/// Java toolchain and lists the direct dep top-level packages as
|
||||
/// dependencies. Each direct dep maps to `<groupId>{pkg}</groupId>`
|
||||
/// with an artifact id matching the package name; this is a best-effort
|
||||
/// stub and Phase 10 corpus expansion will introduce a known-good
|
||||
/// group→artifact registry.
|
||||
pub fn materialize_java(env: &Environment) -> RuntimeArtifacts {
|
||||
let mut artifacts = RuntimeArtifacts::new();
|
||||
let java_version = env
|
||||
.toolchain
|
||||
.version_string
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or("21")
|
||||
.to_owned();
|
||||
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_java_stdlib(d) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(d.clone()) {
|
||||
deps.push(d.clone());
|
||||
}
|
||||
}
|
||||
deps.sort_unstable();
|
||||
|
||||
let mut body = String::with_capacity(256);
|
||||
body.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
body.push_str("<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n");
|
||||
body.push_str(" <modelVersion>4.0.0</modelVersion>\n");
|
||||
body.push_str(" <groupId>nyx</groupId>\n");
|
||||
body.push_str(" <artifactId>harness</artifactId>\n");
|
||||
body.push_str(" <version>0.0.1</version>\n");
|
||||
body.push_str(" <properties>\n");
|
||||
body.push_str(&format!(
|
||||
" <maven.compiler.source>{java_version}</maven.compiler.source>\n"
|
||||
));
|
||||
body.push_str(&format!(
|
||||
" <maven.compiler.target>{java_version}</maven.compiler.target>\n"
|
||||
));
|
||||
body.push_str(" </properties>\n");
|
||||
if !deps.is_empty() {
|
||||
body.push_str(" <dependencies>\n");
|
||||
for d in &deps {
|
||||
body.push_str(" <dependency>\n");
|
||||
body.push_str(&format!(" <groupId>{d}</groupId>\n"));
|
||||
body.push_str(&format!(" <artifactId>{d}</artifactId>\n"));
|
||||
body.push_str(" <version>LATEST</version>\n");
|
||||
body.push_str(" </dependency>\n");
|
||||
}
|
||||
body.push_str(" </dependencies>\n");
|
||||
}
|
||||
body.push_str("</project>\n");
|
||||
artifacts.push("pom.xml", body);
|
||||
artifacts
|
||||
}
|
||||
|
||||
fn is_java_stdlib(name: &str) -> bool {
|
||||
// Best-effort: only `java` / `javax` / `sun` are guaranteed JDK.
|
||||
// `jakarta` ships separately under Jakarta EE so it stays out.
|
||||
// Top-level segments `com` / `org` cover both JDK (`com.sun`) and
|
||||
// third-party (`com.google`, `org.springframework`) — the import
|
||||
// extractor only keeps the first segment, so a richer registry has
|
||||
// to land before we can pin a meaningful Maven artifact from these.
|
||||
// Phase 10 corpus expansion ships that registry.
|
||||
matches!(name, "java" | "javax" | "sun" | "com" | "org" | "jakarta")
|
||||
}
|
||||
|
||||
// ── Public entry: emit() ────────────────────────────────────────────────────
|
||||
|
||||
/// Emit a Java harness for `spec`.
|
||||
///
|
||||
/// Reads `spec.entry_file` from disk (best-effort), resolves the
|
||||
/// concrete [`JavaShape`] via [`JavaShape::detect`], and dispatches to
|
||||
/// the matching per-shape emitter. When the file cannot be read the
|
||||
/// dispatcher falls back to [`JavaShape::StaticMethod`], preserving the
|
||||
/// pre-Phase-14 behaviour.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {}
|
||||
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
|
||||
PayloadSlot::Param(_)
|
||||
| PayloadSlot::EnvVar(_)
|
||||
| PayloadSlot::QueryParam(_)
|
||||
| PayloadSlot::HttpBody
|
||||
| PayloadSlot::Argv(_) => {}
|
||||
PayloadSlot::Stdin => return Err(UnsupportedReason::PayloadSlotUnsupported),
|
||||
}
|
||||
|
||||
let source = generate_harness_java(spec);
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
let shape = JavaShape::detect(spec, &entry_source);
|
||||
let entry_class = derive_entry_class(&entry_source);
|
||||
let source = generate_harness_java(spec, shape, &entry_class);
|
||||
|
||||
Ok(HarnessSource {
|
||||
source,
|
||||
filename: "NyxHarness.java".to_owned(),
|
||||
// Use absolute workdir classpath set by runner.rs after compilation.
|
||||
// Before runner.rs updates it, '.' works for process backend when run
|
||||
// from the workdir.
|
||||
command: vec![
|
||||
"java".to_owned(),
|
||||
"-cp".to_owned(),
|
||||
|
|
@ -293,22 +435,109 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
"NyxHarness".to_owned(),
|
||||
],
|
||||
extra_files: vec![],
|
||||
entry_subpath: Some("Entry.java".to_owned()),
|
||||
// Stage the entry file under the public-class-derived filename
|
||||
// so javac's filename-vs-public-class invariant holds for both
|
||||
// the legacy `public class Entry` fixtures (which keep being
|
||||
// copied to `workdir/Entry.java`) and the Phase 14 shape
|
||||
// fixtures (where `public class Vuln` lives in `Vuln.java`).
|
||||
entry_subpath: Some(format!("{entry_class}.java")),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_harness_java(spec: &HarnessSpec) -> String {
|
||||
let entry_method = &spec.entry_name;
|
||||
let (pre_call, call_expr) = build_call(spec, entry_method);
|
||||
/// Public wrapper to detect the shape for a finalised `HarnessSpec`,
|
||||
/// reading the entry file from disk. Exposed so test helpers can pin a
|
||||
/// per-fixture shape without round-tripping through [`emit`].
|
||||
pub fn detect_shape(spec: &HarnessSpec) -> JavaShape {
|
||||
let entry_source = read_entry_source(&spec.entry_file);
|
||||
JavaShape::detect(spec, &entry_source)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// Locate the harness's target class by parsing the entry source for a
|
||||
/// `public class X` (or `public final class X` / `public abstract class
|
||||
/// X`) declaration. Falls back to `"Entry"` when the source is empty
|
||||
/// or no public-class line is present.
|
||||
///
|
||||
/// The returned name drives both the in-harness invocation
|
||||
/// (`{class}.method(...)` / `Class.forName(class)`) and the
|
||||
/// `entry_subpath` (`{class}.java`) so javac's filename-vs-public-class
|
||||
/// invariant holds for both the legacy `public class Entry` fixtures
|
||||
/// and the Phase 14 shape fixtures that ship `public class Vuln`
|
||||
/// (or `public class Benign`).
|
||||
fn derive_entry_class(source: &str) -> String {
|
||||
parse_public_class_name(source).unwrap_or_else(|| "Entry".to_owned())
|
||||
}
|
||||
|
||||
fn parse_public_class_name(source: &str) -> Option<String> {
|
||||
for line in source.lines() {
|
||||
let l = line.trim_start();
|
||||
let rest = match l
|
||||
.strip_prefix("public class ")
|
||||
.or_else(|| l.strip_prefix("public final class "))
|
||||
.or_else(|| l.strip_prefix("public abstract class "))
|
||||
{
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let name: String = rest
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$')
|
||||
.collect();
|
||||
if !name.is_empty() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ── Per-shape harness generation ────────────────────────────────────────────
|
||||
|
||||
fn generate_harness_java(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> String {
|
||||
let probe = probe_shim();
|
||||
let pre_call = pre_call_setup(spec);
|
||||
let invocation = invoke_for_shape(spec, shape, entry_class);
|
||||
let helpers = shape_helpers(shape);
|
||||
|
||||
// Reflection-driven shapes throw `InvocationTargetException` on
|
||||
// user-code failure; non-reflection shapes (`StaticMethod`,
|
||||
// `StaticMain`) call the entry directly and would surface an
|
||||
// "unreachable catch" javac error if the specific catch clause is
|
||||
// kept. Emit only the broad `Throwable` catch for those shapes.
|
||||
let extra_catch = if shape_uses_reflection(shape) {
|
||||
r#" } catch (InvocationTargetException ite) {
|
||||
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
|
||||
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"// Nyx dynamic harness — auto-generated, do not edit.
|
||||
r#"// Nyx dynamic harness — auto-generated, do not edit (Phase 14 — JavaShape::{shape:?}).
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class NyxHarness {{
|
||||
public static void main(String[] args) throws Exception {{
|
||||
{probe}
|
||||
{helpers}
|
||||
public static void main(String[] args) {{
|
||||
String payload = nyxPayload();
|
||||
{pre_call} try {{
|
||||
{call_expr}
|
||||
}} catch (Exception e) {{
|
||||
{invocation}
|
||||
{extra_catch}}} catch (Throwable e) {{
|
||||
System.err.println("NYX_EXCEPTION: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
}}
|
||||
}}
|
||||
|
|
@ -327,37 +556,226 @@ public class NyxHarness {{
|
|||
}}
|
||||
}}
|
||||
"#,
|
||||
shape = shape,
|
||||
probe = probe,
|
||||
helpers = helpers,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
invocation = invocation,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, method: &str) -> (String, String) {
|
||||
fn pre_call_setup(spec: &HarnessSpec) -> String {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => {
|
||||
let pre = String::new();
|
||||
let call = format!("Entry.{method}(payload);");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
// Use System.setProperty since env vars cannot be set post-JVM-launch
|
||||
// via standard Java APIs. Fixtures that read env vars must use
|
||||
// System.getProperty as a fallback, or read NYX_PAYLOAD_PROP_{name}.
|
||||
let pre = format!(
|
||||
" System.setProperty({name:?}, payload);\n"
|
||||
);
|
||||
let call = format!("Entry.{method}();");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
let pre = String::new();
|
||||
let call = format!("Entry.{method}(payload);");
|
||||
(pre, call)
|
||||
format!(" System.setProperty({name:?}, payload);\n")
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the per-shape entry-invocation block. Shapes that need
|
||||
/// reflection plumbing rely on helpers from [`shape_helpers`].
|
||||
fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> String {
|
||||
let method = spec.entry_name.as_str();
|
||||
match shape {
|
||||
JavaShape::StaticMethod => format!(" {entry_class}.{method}(payload);"),
|
||||
JavaShape::StaticMain => format!(
|
||||
" String[] mainArgs = new String[] {{ payload }};\n {entry_class}.main(mainArgs);"
|
||||
),
|
||||
JavaShape::ServletDoGet => format!(
|
||||
" invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\");"
|
||||
),
|
||||
JavaShape::ServletDoPost => format!(
|
||||
" invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\");"
|
||||
),
|
||||
JavaShape::SpringController => format!(
|
||||
" invokeReflective({entry_class}.class, \"{method}\", payload);"
|
||||
),
|
||||
JavaShape::QuarkusRoute => format!(
|
||||
" invokeReflective({entry_class}.class, \"{method}\", payload);"
|
||||
),
|
||||
JavaShape::JunitTest => format!(
|
||||
" invokeJunitTest({entry_class}.class, \"{method}\");"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-shape helper methods spliced into the harness class.
|
||||
fn shape_helpers(shape: JavaShape) -> &'static str {
|
||||
match shape {
|
||||
JavaShape::StaticMethod | JavaShape::StaticMain => "",
|
||||
JavaShape::ServletDoGet | JavaShape::ServletDoPost => SERVLET_HELPER,
|
||||
JavaShape::SpringController | JavaShape::QuarkusRoute => REFLECTIVE_HELPER,
|
||||
JavaShape::JunitTest => JUNIT_HELPER,
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_uses_reflection(shape: JavaShape) -> bool {
|
||||
!matches!(shape, JavaShape::StaticMethod | JavaShape::StaticMain)
|
||||
}
|
||||
|
||||
/// Reflective servlet invocation. Walks `cls`'s declared methods for a
|
||||
/// match on `methodName` and invokes with `(StubReq, StubResp)`. When
|
||||
/// the fixture's `doGet`/`doPost` takes only a `String` payload (the
|
||||
/// stub-free path used by many fixtures), the helper falls back to
|
||||
/// `invokeReflective`.
|
||||
const SERVLET_HELPER: &str = r#"
|
||||
static void invokeServlet(Class<?> cls, String methodName, String payload, String httpMethod) throws Exception {
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(methodName)) continue;
|
||||
match = m;
|
||||
break;
|
||||
}
|
||||
if (match == null) {
|
||||
throw new NoSuchMethodException(cls.getName() + "." + methodName);
|
||||
}
|
||||
match.setAccessible(true);
|
||||
Object instance = null;
|
||||
if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) {
|
||||
instance = newDefaultInstance(cls);
|
||||
}
|
||||
Class<?>[] params = match.getParameterTypes();
|
||||
Object[] args = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
Class<?> p = params[i];
|
||||
if (p.equals(String.class)) {
|
||||
args[i] = payload;
|
||||
} else if (p.getName().endsWith("HttpServletRequest")) {
|
||||
args[i] = buildRequestStub(p, payload, httpMethod);
|
||||
} else if (p.getName().endsWith("HttpServletResponse")) {
|
||||
args[i] = buildResponseStub(p);
|
||||
} else {
|
||||
args[i] = null;
|
||||
}
|
||||
}
|
||||
match.invoke(instance, args);
|
||||
}
|
||||
|
||||
static Object newDefaultInstance(Class<?> cls) throws Exception {
|
||||
Constructor<?> ctor = cls.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
return ctor.newInstance();
|
||||
}
|
||||
|
||||
static Object buildRequestStub(Class<?> reqType, String payload, String method) throws Exception {
|
||||
// Best-effort: invoke a no-arg constructor and call any
|
||||
// `setParameter`/`setMethod` setters the stub exposes. When
|
||||
// the type cannot be instantiated, fall back to null and let
|
||||
// the fixture handle the missing parameter.
|
||||
try {
|
||||
Constructor<?> ctor = reqType.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
Object stub = ctor.newInstance();
|
||||
try {
|
||||
Method setParam = reqType.getMethod("setParameter", String.class, String.class);
|
||||
setParam.invoke(stub, "payload", payload);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
Method setMethod = reqType.getMethod("setMethod", String.class);
|
||||
setMethod.invoke(stub, method);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
try {
|
||||
Method setBody = reqType.getMethod("setBody", String.class);
|
||||
setBody.invoke(stub, payload);
|
||||
} catch (NoSuchMethodException ignore) {}
|
||||
return stub;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Object buildResponseStub(Class<?> respType) throws Exception {
|
||||
try {
|
||||
Constructor<?> ctor = respType.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
return ctor.newInstance();
|
||||
} catch (NoSuchMethodException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void invokeReflective(Class<?> cls, String methodName, String payload) throws Exception {
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {
|
||||
if (m.getName().equals(methodName)) { match = m; break; }
|
||||
}
|
||||
if (match == null) {
|
||||
throw new NoSuchMethodException(cls.getName() + "." + methodName);
|
||||
}
|
||||
match.setAccessible(true);
|
||||
Object instance = null;
|
||||
if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) {
|
||||
instance = newDefaultInstance(cls);
|
||||
}
|
||||
Class<?>[] params = match.getParameterTypes();
|
||||
Object[] args = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
args[i] = params[i].equals(String.class) ? payload : null;
|
||||
}
|
||||
match.invoke(instance, args);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Reflective Spring / Quarkus invocation. Same shape as the servlet
|
||||
/// reflective fallback but routed through a dedicated helper for
|
||||
/// clarity in the generated harness.
|
||||
const REFLECTIVE_HELPER: &str = r#"
|
||||
static Object newDefaultInstance(Class<?> cls) throws Exception {
|
||||
Constructor<?> ctor = cls.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
return ctor.newInstance();
|
||||
}
|
||||
|
||||
static void invokeReflective(Class<?> cls, String methodName, String payload) throws Exception {
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {
|
||||
if (m.getName().equals(methodName)) { match = m; break; }
|
||||
}
|
||||
if (match == null) {
|
||||
throw new NoSuchMethodException(cls.getName() + "." + methodName);
|
||||
}
|
||||
match.setAccessible(true);
|
||||
Object instance = null;
|
||||
if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) {
|
||||
instance = newDefaultInstance(cls);
|
||||
}
|
||||
Class<?>[] params = match.getParameterTypes();
|
||||
Object[] args = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
args[i] = params[i].equals(String.class) ? payload : null;
|
||||
}
|
||||
match.invoke(instance, args);
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Reflective JUnit-shape invocation. Reads the payload from
|
||||
/// `NYX_PAYLOAD` (no method argument) — JUnit tests typically capture
|
||||
/// inputs through fields or `System.getenv`.
|
||||
const JUNIT_HELPER: &str = r#"
|
||||
static Object newDefaultInstance(Class<?> cls) throws Exception {
|
||||
Constructor<?> ctor = cls.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
return ctor.newInstance();
|
||||
}
|
||||
|
||||
static void invokeJunitTest(Class<?> cls, String methodName) throws Exception {
|
||||
Method match = null;
|
||||
for (Method m : cls.getDeclaredMethods()) {
|
||||
if (m.getName().equals(methodName)) { match = m; break; }
|
||||
}
|
||||
if (match == null) {
|
||||
throw new NoSuchMethodException(cls.getName() + "." + methodName);
|
||||
}
|
||||
match.setAccessible(true);
|
||||
Object instance = null;
|
||||
if (!java.lang.reflect.Modifier.isStatic(match.getModifiers())) {
|
||||
instance = newDefaultInstance(cls);
|
||||
}
|
||||
match.invoke(instance);
|
||||
}
|
||||
"#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -396,7 +814,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn emit_entry_subpath_is_entry_java() {
|
||||
fn emit_entry_subpath_default_static_method_is_entry_java() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
||||
|
|
@ -411,10 +829,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn emit_param_gt_0_is_unsupported() {
|
||||
fn emit_param_gt_0_is_accepted_for_static_method() {
|
||||
// Phase 14: PayloadSlot::Param(n>0) is no longer rejected; the
|
||||
// emitter routes the payload via the first-arg slot regardless
|
||||
// (the runner has already pinned the slot at spec time).
|
||||
let spec = make_spec(PayloadSlot::Param(1));
|
||||
let err = emit(&spec).unwrap_err();
|
||||
assert_eq!(err, UnsupportedReason::PayloadSlotUnsupported);
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert!(harness.source.contains("processInput(payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -430,13 +851,19 @@ mod tests {
|
|||
assert!(JavaEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
assert!(JavaEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::HttpRoute));
|
||||
assert!(JavaEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::CliSubcommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = JavaEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 14"));
|
||||
let hint = JavaEmitter.entry_kind_hint(EntryKind::LibraryApi);
|
||||
assert!(hint.contains("LibraryApi"));
|
||||
assert!(hint.contains("Phase 14"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -446,4 +873,120 @@ mod tests {
|
|||
assert!(harness.source.contains("Base64.getDecoder()"));
|
||||
assert!(harness.source.contains("NYX_PAYLOAD_B64"));
|
||||
}
|
||||
|
||||
// ── Phase 14: 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_servlet_doget() {
|
||||
let src = "import javax.servlet.http.HttpServletRequest;\npublic class V extends HttpServlet { public void doGet(HttpServletRequest r, HttpServletResponse w) {} }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "doGet", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::ServletDoGet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_servlet_dopost() {
|
||||
let src = "import jakarta.servlet.http.HttpServletRequest;\npublic class V extends HttpServlet { public void doPost(HttpServletRequest r, HttpServletResponse w) {} }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "doPost", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::ServletDoPost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_spring_controller() {
|
||||
let src = "@RestController\npublic class V { @GetMapping(\"/x\") public String run(String p) { return p; } }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::SpringController);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_quarkus_route() {
|
||||
let src = "import jakarta.ws.rs.GET;\n@Path(\"/x\")\npublic class V { @GET public String run(String p) { return p; } }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::QuarkusRoute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_static_main() {
|
||||
let src = "public class V { public static void main(String[] args) {} }";
|
||||
let spec = make_spec_with(EntryKind::CliSubcommand, "main", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::StaticMain);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_junit_test() {
|
||||
let src = "import org.junit.jupiter.api.Test;\npublic class V { @Test public void testRun() {} }";
|
||||
let spec = make_spec_with(EntryKind::Function, "testRun", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::JunitTest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_static_method_fallback() {
|
||||
let src = "public class V { public static void run(String p) {} }";
|
||||
let spec = make_spec_with(EntryKind::Function, "run", "V.java");
|
||||
assert_eq!(JavaShape::detect(&spec, src), JavaShape::StaticMethod);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn servlet_shape_emits_reflective_invocation() {
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "doGet", "Vuln.java");
|
||||
let src = generate_harness_java(&spec, JavaShape::ServletDoGet, "Vuln");
|
||||
assert!(src.contains("invokeServlet(Vuln.class"));
|
||||
assert!(src.contains("buildRequestStub"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spring_shape_emits_reflective_invocation() {
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java");
|
||||
let src = generate_harness_java(&spec, JavaShape::SpringController, "Vuln");
|
||||
assert!(src.contains("invokeReflective(Vuln.class, \"run\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quarkus_shape_emits_reflective_invocation() {
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "run", "Vuln.java");
|
||||
let src = generate_harness_java(&spec, JavaShape::QuarkusRoute, "Vuln");
|
||||
assert!(src.contains("invokeReflective(Vuln.class, \"run\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_main_shape_passes_argv() {
|
||||
let spec = make_spec_with(EntryKind::CliSubcommand, "main", "Vuln.java");
|
||||
let src = generate_harness_java(&spec, JavaShape::StaticMain, "Vuln");
|
||||
assert!(src.contains("Vuln.main(mainArgs)"));
|
||||
assert!(src.contains("new String[] { payload }"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn junit_shape_emits_reflective_invocation() {
|
||||
let spec = make_spec_with(EntryKind::Function, "testRun", "Vuln.java");
|
||||
let src = generate_harness_java(&spec, JavaShape::JunitTest, "Vuln");
|
||||
assert!(src.contains("invokeJunitTest(Vuln.class"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_class_parses_public_class_declaration() {
|
||||
assert_eq!(derive_entry_class("public class Vuln {}"), "Vuln");
|
||||
assert_eq!(derive_entry_class("public final class Foo {}"), "Foo");
|
||||
assert_eq!(derive_entry_class("public abstract class Bar {}"), "Bar");
|
||||
// No public class → "Entry" fallback.
|
||||
assert_eq!(derive_entry_class(""), "Entry");
|
||||
assert_eq!(derive_entry_class("class Pkg {}"), "Entry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_subpath_matches_public_class() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
// Path does not exist on disk → derive_entry_class falls back
|
||||
// to "Entry" → subpath is "Entry.java".
|
||||
spec.entry_file = "/nonexistent/Vuln.java".into();
|
||||
let harness = emit(&spec).unwrap();
|
||||
assert_eq!(harness.entry_subpath, Some("Entry.java".to_owned()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,11 +307,53 @@ pub fn run_shape_fixture_lang(
|
|||
constraint_hints: vec![],
|
||||
sink_file: entry_file,
|
||||
sink_line,
|
||||
spec_hash,
|
||||
spec_hash: spec_hash.clone(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
};
|
||||
|
||||
// Phase 14: Java shape fixtures bundle annotation / type stubs as
|
||||
// sibling `*.java` files alongside `Vuln.java` / `Benign.java`.
|
||||
// The harness builder owns `/tmp/nyx-harness/<spec_hash>/` and only
|
||||
// copies the entry file + extra_files — it never walks the entry
|
||||
// file's parent dir. Pre-create the workdir and stage every
|
||||
// sibling stub there so the build sandbox's `javac *.java` step
|
||||
// resolves the annotation / type references without pulling in any
|
||||
// Maven deps. Skip the alternate Vuln/Benign file to keep public
|
||||
// class declarations from colliding with the running variant.
|
||||
if matches!(lang, nyx_scanner::symbol::Lang::Java) {
|
||||
let workdir = std::path::PathBuf::from("/tmp/nyx-harness").join(&spec.spec_hash);
|
||||
// Wipe any prior contents so stale `.java` / `.class` files
|
||||
// from previous emitter revisions cannot bleed into this run.
|
||||
// `prepare_java` globs every `*.java` in the workdir — leaving
|
||||
// an obsolete `Entry.java` next to the new `Vuln.java` produces
|
||||
// a duplicate-class compile error.
|
||||
let _ = std::fs::remove_dir_all(&workdir);
|
||||
let _ = std::fs::create_dir_all(&workdir);
|
||||
let alt_file = if file == "Vuln.java" {
|
||||
"Benign.java"
|
||||
} else if file == "Benign.java" {
|
||||
"Vuln.java"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if let Ok(entries) = std::fs::read_dir(&fixture_root) {
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
let name = match p.file_name().and_then(|n| n.to_str()) {
|
||||
Some(n) => n.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
if name == file || name == alt_file {
|
||||
continue;
|
||||
}
|
||||
if p.extension().map(|e| e == "java").unwrap_or(false) {
|
||||
let _ = std::fs::copy(&p, workdir.join(&name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = run_spec(&spec, &opts);
|
||||
|
||||
|
|
|
|||
24
tests/dynamic_fixtures/java/junit_test/Benign.java
Normal file
24
tests/dynamic_fixtures/java/junit_test/Benign.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 14 — JUnit test method, benign.
|
||||
|
||||
// import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Benign {
|
||||
@Test
|
||||
public void testRun() throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
// Read + drop payload.
|
||||
String unused = System.getenv("NYX_PAYLOAD");
|
||||
if (unused == null) unused = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/java/junit_test/Test.java
Normal file
15
tests/dynamic_fixtures/java/junit_test/Test.java
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 14 fixture stub — minimal `@Test` annotation in the default
|
||||
// package. Lives here so the fixture's `@Test`-annotated method
|
||||
// compiles under plain javac without a junit-jupiter Maven dep. The
|
||||
// fixture's comment carries a literal `org.junit` marker so the
|
||||
// Phase 14 [`JavaShape::detect`] still selects the JUnit shape.
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Test {
|
||||
}
|
||||
28
tests/dynamic_fixtures/java/junit_test/Vuln.java
Normal file
28
tests/dynamic_fixtures/java/junit_test/Vuln.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 14 — JUnit test method, vulnerable.
|
||||
//
|
||||
// The `org.junit.jupiter.api` comment marker tells the Phase 14 shape
|
||||
// detector to select `JavaShape::JunitTest`; the actual annotation is
|
||||
// the fixture-local `@NyxTest` stub so the file compiles under a
|
||||
// dependency-free javac invocation.
|
||||
|
||||
// import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Vuln {
|
||||
@Test
|
||||
public void testRun() throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String input = System.getenv("NYX_PAYLOAD");
|
||||
if (input == null) input = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + input};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/java/junit_test/pom.xml
Normal file
19
tests/dynamic_fixtures/java/junit_test/pom.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>junit-test-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
27
tests/dynamic_fixtures/java/quarkus_route/Benign.java
Normal file
27
tests/dynamic_fixtures/java/quarkus_route/Benign.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 14 — Quarkus reactive route, benign.
|
||||
|
||||
// import io.quarkus.runtime.Quarkus;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
@Path("/run")
|
||||
public class Benign {
|
||||
@GET
|
||||
public String run(String payload) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
if (payload == null) payload = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
StringBuilder out = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
out.append(line);
|
||||
out.append('\n');
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/java/quarkus_route/GET.java
Normal file
11
tests/dynamic_fixtures/java/quarkus_route/GET.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 14 fixture stub — minimal `@GET` Jakarta REST annotation.
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface GET {
|
||||
}
|
||||
15
tests/dynamic_fixtures/java/quarkus_route/Path.java
Normal file
15
tests/dynamic_fixtures/java/quarkus_route/Path.java
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 14 fixture stub — minimal `@Path` annotation (Jakarta REST).
|
||||
// Lives in the default package; the fixture imports the symbol as
|
||||
// plain `@Path` so javac is happy without a Quarkus / Jakarta REST
|
||||
// Maven dep.
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface Path {
|
||||
String value() default "";
|
||||
}
|
||||
31
tests/dynamic_fixtures/java/quarkus_route/Vuln.java
Normal file
31
tests/dynamic_fixtures/java/quarkus_route/Vuln.java
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Phase 14 — Quarkus reactive route, vulnerable.
|
||||
//
|
||||
// `@Path("/run")` on the type + `@GET` on the handler matches the
|
||||
// Phase 14 [`JavaShape::detect`] for Quarkus. The harness invokes
|
||||
// `run(payload)` via reflection.
|
||||
|
||||
// import io.quarkus.runtime.Quarkus;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
@Path("/run")
|
||||
public class Vuln {
|
||||
@GET
|
||||
public String run(String payload) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
if (payload == null) payload = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + payload};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
StringBuilder out = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
out.append(line);
|
||||
out.append('\n');
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
18
tests/dynamic_fixtures/java/quarkus_route/pom.xml
Normal file
18
tests/dynamic_fixtures/java/quarkus_route/pom.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>quarkus-route-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||
<version>3.8.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
24
tests/dynamic_fixtures/java/servlet_doget/Benign.java
Normal file
24
tests/dynamic_fixtures/java/servlet_doget/Benign.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 14 — servlet doGet, benign.
|
||||
//
|
||||
// Reads `payload` from the request but never threads it into a
|
||||
// shell-interpreted slot; the cmdi marker cannot fire.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Benign {
|
||||
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
// Read + drop the parameter.
|
||||
String unused = req.getParameter("payload");
|
||||
if (unused == null) unused = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 14 fixture stub — minimal servlet request shape.
|
||||
// Lives in the default package so the harness shim's
|
||||
// `p.getName().endsWith("HttpServletRequest")` filter can match without
|
||||
// a Maven dep on `jakarta.servlet-api`.
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpServletRequest {
|
||||
private final Map<String, String> params = new HashMap<>();
|
||||
private String method = "GET";
|
||||
private String body = "";
|
||||
|
||||
public void setParameter(String k, String v) { params.put(k, v); }
|
||||
public String getParameter(String k) { return params.get(k); }
|
||||
public void setMethod(String m) { this.method = m; }
|
||||
public String getMethod() { return method; }
|
||||
public void setBody(String b) { this.body = b; }
|
||||
public String getBody() { return body; }
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Phase 14 fixture stub — minimal servlet response shape.
|
||||
public class HttpServletResponse {
|
||||
private final StringBuilder body = new StringBuilder();
|
||||
public void write(String s) { body.append(s); }
|
||||
public String getBody() { return body.toString(); }
|
||||
}
|
||||
24
tests/dynamic_fixtures/java/servlet_doget/Vuln.java
Normal file
24
tests/dynamic_fixtures/java/servlet_doget/Vuln.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Phase 14 — servlet doGet, vulnerable.
|
||||
//
|
||||
// Reads the `payload` query parameter from the request stub and feeds
|
||||
// it through `/bin/sh -c` — payload `; echo NYX_PWN_CMDI` fires the
|
||||
// cmdi oracle marker.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Vuln {
|
||||
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String input = req.getParameter("payload");
|
||||
if (input == null) input = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + input};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/java/servlet_doget/pom.xml
Normal file
19
tests/dynamic_fixtures/java/servlet_doget/pom.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>servlet-doget-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
20
tests/dynamic_fixtures/java/servlet_dopost/Benign.java
Normal file
20
tests/dynamic_fixtures/java/servlet_dopost/Benign.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 14 — servlet doPost, benign.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Benign {
|
||||
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String unused = req.getBody();
|
||||
if (unused == null) unused = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 14 fixture stub — minimal servlet request shape.
|
||||
// Lives in the default package so the harness shim's
|
||||
// `p.getName().endsWith("HttpServletRequest")` filter can match without
|
||||
// a Maven dep on `jakarta.servlet-api`.
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpServletRequest {
|
||||
private final Map<String, String> params = new HashMap<>();
|
||||
private String method = "GET";
|
||||
private String body = "";
|
||||
|
||||
public void setParameter(String k, String v) { params.put(k, v); }
|
||||
public String getParameter(String k) { return params.get(k); }
|
||||
public void setMethod(String m) { this.method = m; }
|
||||
public String getMethod() { return method; }
|
||||
public void setBody(String b) { this.body = b; }
|
||||
public String getBody() { return body; }
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Phase 14 fixture stub — minimal servlet response shape.
|
||||
public class HttpServletResponse {
|
||||
private final StringBuilder body = new StringBuilder();
|
||||
public void write(String s) { body.append(s); }
|
||||
public String getBody() { return body.toString(); }
|
||||
}
|
||||
23
tests/dynamic_fixtures/java/servlet_dopost/Vuln.java
Normal file
23
tests/dynamic_fixtures/java/servlet_dopost/Vuln.java
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 14 — servlet doPost, vulnerable.
|
||||
//
|
||||
// Reads the POST body from the request stub and feeds it through
|
||||
// `/bin/sh -c`.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Vuln {
|
||||
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String input = req.getBody();
|
||||
if (input == null) input = "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + input};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/java/servlet_dopost/pom.xml
Normal file
19
tests/dynamic_fixtures/java/servlet_dopost/pom.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>servlet-dopost-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
13
tests/dynamic_fixtures/java/spring_controller/Autowired.java
Normal file
13
tests/dynamic_fixtures/java/spring_controller/Autowired.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 14 fixture stub — minimal `@Autowired` annotation.
|
||||
// Lives in the default package so the fixture's @Autowired field
|
||||
// compiles under plain javac (no Spring Maven dep required).
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
|
||||
public @interface Autowired {
|
||||
}
|
||||
19
tests/dynamic_fixtures/java/spring_controller/Benign.java
Normal file
19
tests/dynamic_fixtures/java/spring_controller/Benign.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 14 — Spring `@RestController`, benign.
|
||||
//
|
||||
// Same shape as the vuln but the controller runs a fixed echo and
|
||||
// drops `payload`.
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/run")
|
||||
public class Benign {
|
||||
@Autowired
|
||||
private CommandRunner runner;
|
||||
|
||||
public String run(String payload) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
CommandRunner r = (runner != null) ? runner : new CommandRunner();
|
||||
String out = r.run("echo hello");
|
||||
System.out.print(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 14 fixture stub — Spring-injected helper service.
|
||||
// The fixture's controller declares `@Autowired CommandRunner runner;`
|
||||
// so the harness exercises the Phase 09 import-extraction path
|
||||
// (`@Autowired` is the marker that flags `org.springframework` as a
|
||||
// transitive dep). At runtime the harness instantiates the controller
|
||||
// via reflection's default ctor — the @Autowired field stays null
|
||||
// because there is no Spring container; the controller's handler
|
||||
// guards against null and constructs a fresh CommandRunner on demand.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class CommandRunner {
|
||||
public String run(String cmd) throws Exception {
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
StringBuilder out = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
out.append(line);
|
||||
out.append('\n');
|
||||
}
|
||||
p.waitFor();
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 14 fixture stub — minimal Spring `@RequestMapping`.
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface RequestMapping {
|
||||
String value() default "";
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Phase 14 fixture stub — minimal Spring `@RestController`.
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface RestController {
|
||||
}
|
||||
22
tests/dynamic_fixtures/java/spring_controller/Vuln.java
Normal file
22
tests/dynamic_fixtures/java/spring_controller/Vuln.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 14 — Spring `@RestController`, vulnerable.
|
||||
//
|
||||
// Controller declares an `@Autowired CommandRunner` field so the
|
||||
// Phase 09 Java import-extractor sees the Spring annotation surface.
|
||||
// The harness instantiates the controller via reflection and invokes
|
||||
// `run(payload)`; the field stays null at runtime (no Spring DI), so
|
||||
// the handler constructs the helper on demand.
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/run")
|
||||
public class Vuln {
|
||||
@Autowired
|
||||
private CommandRunner runner;
|
||||
|
||||
public String run(String payload) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
CommandRunner r = (runner != null) ? runner : new CommandRunner();
|
||||
String out = r.run("echo hello " + payload);
|
||||
System.out.print(out);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
23
tests/dynamic_fixtures/java/spring_controller/pom.xml
Normal file
23
tests/dynamic_fixtures/java/spring_controller/pom.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>spring-controller-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<version>6.1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>6.1.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
21
tests/dynamic_fixtures/java/static_main/Benign.java
Normal file
21
tests/dynamic_fixtures/java/static_main/Benign.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 14 — static `main(String[])` entry, benign.
|
||||
//
|
||||
// Discards `args[0]` and runs a fixed echo — payload never reaches the
|
||||
// shell-interpreted slot so the cmdi marker cannot fire.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Benign {
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
22
tests/dynamic_fixtures/java/static_main/Vuln.java
Normal file
22
tests/dynamic_fixtures/java/static_main/Vuln.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 14 — static `main(String[])` entry, vulnerable.
|
||||
//
|
||||
// Payload arrives as `args[0]` and lands in a shell-interpreted
|
||||
// `Runtime.exec` invocation.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Vuln {
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String input = args.length > 0 ? args[0] : "";
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + input};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/java/static_main/pom.xml
Normal file
11
tests/dynamic_fixtures/java/static_main/pom.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>static-main-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
23
tests/dynamic_fixtures/java/static_method/Benign.java
Normal file
23
tests/dynamic_fixtures/java/static_method/Benign.java
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 14 — plain static method, benign.
|
||||
//
|
||||
// Invokes a fixed shell command and discards the user input — the `;`
|
||||
// in a vuln payload cannot escape because the payload is never passed
|
||||
// to a shell-interpreted argv slot.
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Benign {
|
||||
public static void processInput(String input) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
// No-op echo of a fixed string — `input` is dropped.
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello"};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/java/static_method/Vuln.java
Normal file
21
tests/dynamic_fixtures/java/static_method/Vuln.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 14 — plain static method, vulnerable.
|
||||
//
|
||||
// JDK-only. Passes user input through `/bin/sh -c` so a `;` in the
|
||||
// payload escapes into a new command (CMDI oracle marker fires).
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Vuln {
|
||||
public static void processInput(String input) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + input};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/java/static_method/pom.xml
Normal file
14
tests/dynamic_fixtures/java/static_method/pom.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Phase 14 fixture pom.xml — pins JDK 17 for materialize_java parity.
|
||||
Not consumed by prepare_java (which shells out to `javac` directly);
|
||||
present so the Phase 09 Track D.2 dep-emission path has a reference. -->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>nyx</groupId>
|
||||
<artifactId>static-method-fixture</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
|
|
@ -1,14 +1,24 @@
|
|||
//! Java fixture integration tests (Phase 05 acceptance gate).
|
||||
//! Java fixture integration tests (Phase 05 acceptance gate + Phase 14
|
||||
//! per-shape acceptance).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each Java fixture and asserts
|
||||
//! the expected verdict. Requires `--features dynamic` and `java`/`javac` on PATH.
|
||||
//! Phase 05 surface: runs `verify_finding` against each legacy
|
||||
//! `tests/dynamic_fixtures/java/<name>.java` (entry class `Entry`,
|
||||
//! `public static void <fn>(String)`) and asserts the expected verdict.
|
||||
//!
|
||||
//! Entry points follow: `public static void FuncName(String)` in class `Entry`.
|
||||
//! The harness wraps each fixture in a generated `NyxHarness.java` that reads
|
||||
//! `NYX_PAYLOAD` and calls `Entry.FuncName(payload)`.
|
||||
//! Phase 14 surface (`#[cfg(feature = "dynamic")] mod phase14_shape_tests`):
|
||||
//! for each [`nyx_scanner::dynamic::lang::java::JavaShape`] asserts
|
||||
//! `Confirmed` on the vuln fixture and `NotConfirmed` on the benign
|
||||
//! fixture under the `tests/dynamic_fixtures/java/<shape>/` directory.
|
||||
//!
|
||||
//! Prerequisites: `requires: docker-or-jdk17` — the suite skips cleanly
|
||||
//! when `javac` / `java` is unavailable on the host (Phase 29 will wire
|
||||
//! the structured prereq system; for now the suite checks
|
||||
//! `java --version` exit status and returns early on failure).
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test java_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod java_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
|
|
@ -446,3 +456,364 @@ mod java_fixture_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 14: per-shape acceptance ───────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase14_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn java_available() -> bool {
|
||||
std::process::Command::new("javac")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
&& std::process::Command::new("java")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Java, "java", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── static_method ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn static_method_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"static_method", "Vuln.java", "processInput", Cap::CODE_EXEC, 12,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("static_method", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_method_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"static_method", "Benign.java", "processInput", Cap::CODE_EXEC, 13,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("static_method", &r);
|
||||
}
|
||||
|
||||
// ── static_main ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn static_main_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"static_main", "Vuln.java", "main", Cap::CODE_EXEC, 13,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("static_main", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_main_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"static_main", "Benign.java", "main", Cap::CODE_EXEC, 12,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("static_main", &r);
|
||||
}
|
||||
|
||||
// ── servlet_doget ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn servlet_doget_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"servlet_doget", "Vuln.java", "doGet", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_confirmed("servlet_doget", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn servlet_doget_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"servlet_doget", "Benign.java", "doGet", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_not_confirmed("servlet_doget", &r);
|
||||
}
|
||||
|
||||
// ── servlet_dopost ───────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn servlet_dopost_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"servlet_dopost", "Vuln.java", "doPost", Cap::CODE_EXEC, 13,
|
||||
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
||||
);
|
||||
assert_confirmed("servlet_dopost", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn servlet_dopost_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"servlet_dopost", "Benign.java", "doPost", Cap::CODE_EXEC, 12,
|
||||
EntryKind::HttpRoute, PayloadSlot::HttpBody,
|
||||
);
|
||||
assert_not_confirmed("servlet_dopost", &r);
|
||||
}
|
||||
|
||||
// ── spring_controller ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn spring_controller_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"spring_controller", "Vuln.java", "run", Cap::CODE_EXEC, 16,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("spring_controller", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spring_controller_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"spring_controller", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("spring_controller", &r);
|
||||
}
|
||||
|
||||
// ── junit_test ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn junit_test_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"junit_test", "Vuln.java", "testRun", Cap::CODE_EXEC, 17,
|
||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_confirmed("junit_test", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn junit_test_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"junit_test", "Benign.java", "testRun", Cap::CODE_EXEC, 15,
|
||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_not_confirmed("junit_test", &r);
|
||||
}
|
||||
|
||||
// ── quarkus_route ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn quarkus_route_vuln_is_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"quarkus_route", "Vuln.java", "run", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("quarkus_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quarkus_route_benign_not_confirmed() {
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: javac/java not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"quarkus_route", "Benign.java", "run", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("quarkus_route", &r);
|
||||
}
|
||||
|
||||
// ── Phase 09 staging assertion (Spring transitive dep pick-up) ──────────
|
||||
|
||||
/// Verify the Phase 09 staging path identifies Spring when the
|
||||
/// source carries an `@Autowired`-style import line. This is the
|
||||
/// literal Phase 14 acceptance bullet: "Spring fixture exercises
|
||||
/// `@Autowired` to validate the Phase 09 staging picks up
|
||||
/// transitive deps."
|
||||
///
|
||||
/// The Spring fixture itself uses default-package stubs at runtime
|
||||
/// (so plain `javac` can compile it) — this test exercises the
|
||||
/// import-extraction path against a Spring-shaped source snippet
|
||||
/// independent of the runtime path.
|
||||
#[test]
|
||||
fn phase09_staging_picks_up_spring_autowired_imports() {
|
||||
use nyx_scanner::dynamic::environment::capture_project_dependencies;
|
||||
use nyx_scanner::dynamic::lang::java::materialize_java;
|
||||
use nyx_scanner::dynamic::spec::{
|
||||
EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy,
|
||||
};
|
||||
use std::io::Write;
|
||||
|
||||
let project_root = tempfile::TempDir::new().expect("tempdir");
|
||||
let entry_path = project_root.path().join("App.java");
|
||||
{
|
||||
let mut f = std::fs::File::create(&entry_path).unwrap();
|
||||
f.write_all(
|
||||
br#"import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/run")
|
||||
public class App {
|
||||
@Autowired
|
||||
private CommandRunner runner;
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let spec = HarnessSpec {
|
||||
finding_id: "phase14staging00".into(),
|
||||
entry_file: "App.java".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::HttpRoute,
|
||||
lang: Lang::Java,
|
||||
toolchain_id: "java-17".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::CODE_EXEC,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "App.java".into(),
|
||||
sink_line: 8,
|
||||
spec_hash: "phase14staging00".into(),
|
||||
derivation: SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
};
|
||||
|
||||
let captured = capture_project_dependencies(project_root.path(), &spec);
|
||||
assert!(
|
||||
captured.direct_deps.iter().any(|d| d == "org"),
|
||||
"capture_project_dependencies must surface the `org` segment \
|
||||
from Spring imports; got {:?}",
|
||||
captured.direct_deps,
|
||||
);
|
||||
|
||||
// Stage to a workdir + materialize the manifest to round-trip
|
||||
// the dep through the Phase 09 emitter chain. Note: the
|
||||
// current `is_java_stdlib` filter rejects `org` / `com` /
|
||||
// `jakarta` because the Phase 09 import extractor only retains
|
||||
// the first dotted segment, which is ambiguous between JDK and
|
||||
// third-party. Phase 14's contract is "staging picks up the
|
||||
// dep" — the dep landing in `env.direct_deps` is the
|
||||
// observable promise; promoting it to a real `<groupId>` lives
|
||||
// behind the richer-registry follow-up in deferred.md.
|
||||
let workdir = tempfile::TempDir::new().expect("tempdir");
|
||||
let env = nyx_scanner::dynamic::environment::stage_workdir_full(
|
||||
&captured,
|
||||
workdir.path(),
|
||||
&spec.spec_hash,
|
||||
Lang::Java,
|
||||
)
|
||||
.expect("stage_workdir_full");
|
||||
assert!(
|
||||
env.direct_deps.iter().any(|d| d == "org"),
|
||||
"env.direct_deps must carry the captured `org` segment; got {:?}",
|
||||
env.direct_deps,
|
||||
);
|
||||
let artifacts = materialize_java(&env);
|
||||
let pom = artifacts
|
||||
.files
|
||||
.iter()
|
||||
.find(|(p, _)| p == "pom.xml")
|
||||
.expect("materialize_java emits pom.xml");
|
||||
assert!(
|
||||
pom.1.contains("<project"),
|
||||
"pom.xml must be well-formed XML; got: {}",
|
||||
pom.1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue