[pitboss/grind] deferred session-0005 (20260520T233019Z-6958)

This commit is contained in:
pitboss 2026-05-20 22:31:58 -05:00
parent 787da2975f
commit c885a8d424
4 changed files with 150 additions and 21 deletions

View file

@ -570,6 +570,14 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
let _ = std::fs::copy(&src, &dst);
}
}
// Restore cached Maven-resolved jars when the harness shipped a
// `pom.xml`; the harness command embeds `-cp .:lib/*` so the
// runtime classpath needs these jars staged in the workdir.
let cached_lib = cache_path.join("lib");
let workdir_lib = workdir.join("lib");
if cached_lib.exists() && !workdir_lib.exists() {
let _ = copy_dir_all(&cached_lib, &workdir_lib);
}
return Ok(BuildResult {
venv_path: cache_path,
cache_hit: true,
@ -647,6 +655,15 @@ fn java_target_release(toolchain_id: &str) -> Option<u32> {
fn try_compile_java(workdir: &Path, cache_path: &Path, target_release: Option<u32>) -> Result<(), String> {
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
// If the harness emitter shipped a `pom.xml`, stage Maven-resolved
// jars under `workdir/lib` so javac (and the runtime classpath
// baked into the harness command) can resolve framework imports
// like `org.thymeleaf.*`.
let lib_on_cp = workdir.join("pom.xml").exists() && {
fetch_maven_deps(workdir)?;
workdir.join("lib").exists()
};
let sources = collect_java_sources(workdir);
if sources.is_empty() {
return Err("no Java sources found in workdir".to_owned());
@ -658,6 +675,10 @@ fn try_compile_java(workdir: &Path, cache_path: &Path, target_release: Option<u3
args.push("--release".to_owned());
args.push(rel.to_string());
}
if lib_on_cp {
args.push("-cp".to_owned());
args.push(".:lib/*".to_owned());
}
for src in &sources {
args.push(src.to_string_lossy().into_owned());
}
@ -688,6 +709,50 @@ fn try_compile_java(workdir: &Path, cache_path: &Path, target_release: Option<u3
let _ = std::fs::copy(&src, &dst);
}
}
// Persist Maven-resolved jars alongside the class cache so cache-hit
// restores can rebuild the `lib/` classpath without re-running mvn.
if lib_on_cp {
let lib_src = workdir.join("lib");
if lib_src.exists() {
let _ = copy_dir_all(&lib_src, &cache_path.join("lib"));
}
}
Ok(())
}
/// Resolve the `pom.xml` declared dependencies into `workdir/lib`.
///
/// Runs `mvn dependency:copy-dependencies` on the host, scoped to
/// runtime + compile so a transitive test-scoped jar can not bleed
/// into the harness classpath. Honors `NYX_MAVEN_BIN` so CI hosts
/// with a pinned Maven install can override the binary lookup.
///
/// Returns `Err` with the Maven output on failure so the harness
/// build path can surface it as `BuildFailed` upstream.
fn fetch_maven_deps(workdir: &Path) -> Result<(), String> {
let mvn = std::env::var("NYX_MAVEN_BIN").unwrap_or_else(|_| "mvn".to_owned());
let output = Command::new(&mvn)
.args([
"-q",
"-B",
"dependency:copy-dependencies",
"-DoutputDirectory=lib",
"-DincludeScope=runtime",
])
.current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output()
.map_err(|e| format!("mvn dependency:copy-dependencies: {e}"))?;
if !output.status.success() {
let mut msg = String::from_utf8_lossy(&output.stderr).into_owned();
if msg.is_empty() {
msg = String::from_utf8_lossy(&output.stdout).into_owned();
}
return Err(format!("mvn dependency:copy-dependencies failed: {msg}"));
}
Ok(())
}
@ -746,6 +811,13 @@ fn compute_java_source_hash(workdir: &Path, target_release: Option<u32>) -> Stri
h.update(&content);
}
}
// Fold the harness `pom.xml` into the hash so a manifest edit (a
// new dep, a version bump) busts the build cache and re-runs
// `mvn dependency:copy-dependencies` on the next build.
if let Ok(pom) = std::fs::read(workdir.join("pom.xml")) {
h.update(b":pom=");
h.update(&pom);
}
// Fold the target release into the hash so a workdir compiled at
// `--release 17` cannot collide with the same workdir at `--release 21`.
if let Some(rel) = target_release {

View file

@ -827,13 +827,11 @@ pub fn emit_ssti_harness(_spec: &HarnessSpec) -> HarnessSource {
// control `7*7` has no `[[${{ ... }}]]` markers so the engine echoes
// it verbatim.
//
// Compile + classpath bootstrap is handled by the brief's Maven
// addendum — the synthetic harness this replaces never linked
// Thymeleaf, so the build path needs `pom.xml` plumbing routed
// through `prepare_java` before a host without `org.thymeleaf`
// on the classpath can run the harness. Until that plumbing
// lands the e2e Java SSTI test SKIPs via the runner's BuildFailed
// branch.
// The companion `pom.xml` (shipped via `HarnessSource::extra_files`)
// declares the Thymeleaf dependency; `prepare_java` runs
// `mvn dependency:copy-dependencies -DoutputDirectory=lib` against
// any workdir that carries a `pom.xml`, then folds `lib/*` into the
// javac and runtime classpath via the `-cp` arg below.
import java.io.FileWriter;
import java.io.IOException;
import org.thymeleaf.TemplateEngine;
@ -897,14 +895,47 @@ public class NyxHarness {{
command: vec![
"java".to_owned(),
"-cp".to_owned(),
".".to_owned(),
".:lib/*".to_owned(),
"NyxHarness".to_owned(),
],
extra_files: Vec::new(),
extra_files: vec![("pom.xml".to_owned(), ssti_thymeleaf_pom().to_owned())],
entry_subpath: None,
}
}
/// `pom.xml` manifest for the SSTI Thymeleaf harness.
///
/// Declares `org.thymeleaf:thymeleaf:3.1.x` so `prepare_java` can resolve
/// the runtime classpath via `mvn dependency:copy-dependencies` before
/// the javac step. The Thymeleaf 3.1 line is the current LTS branch and
/// the lowest Java baseline (`java 11`) we still target across the test
/// matrix.
fn ssti_thymeleaf_pom() -> &'static str {
r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nyx</groupId>
<artifactId>nyx-harness-thymeleaf</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
</dependencies>
</project>
"#
}
/// Phase 05 — Track J.3 XXE harness for Java (`DocumentBuilderFactory`).
///
/// Reads `NYX_PAYLOAD`, scans for `<!ENTITY name SYSTEM "uri">`

View file

@ -120,13 +120,16 @@ mod parity_tests {
/// Assert two verdicts agree on status (and on reason for non-Confirmed).
fn assert_parity(fixture: &str, process_result: &nyx_scanner::evidence::VerifyResult,
docker_result: &nyx_scanner::evidence::VerifyResult) {
// If docker backend is unavailable, docker result will be Unsupported.
// That's acceptable — we can't compare when docker is missing.
if docker_result.status == VerifyStatus::Unsupported {
if let Some(ref r) = docker_result.reason {
if format!("{r:?}").contains("BackendUnavailable") {
return; // Docker absent — skip comparison.
}
// Docker reachability fluctuates per host: `docker info` may exit 0
// (daemon listening) while the sandbox's container-start path still
// fails (image not pulled, socket gated by Docker Desktop's
// privileged-mode toggle, etc.). The downstream verifier folds
// BackendUnavailable into Unsupported OR Inconclusive depending on
// where the error surfaces, so the skip predicate looks at the
// reason text, not the verdict status.
if let Some(ref r) = docker_result.reason {
if format!("{r:?}").contains("BackendUnavailable") {
return; // Docker absent — skip comparison.
}
}

View file

@ -322,10 +322,11 @@ fn slug(lang: Lang) -> &'static str {
// `ProbePredicate::TemplateEvalEqual { expected: 49 }` → differential
// pair against the `7*7` benign control.
//
// Java is skipped: the Thymeleaf fixture imports `org.thymeleaf.*`
// which is not on the JDK stdlib, so `javac *.java` over the workdir
// fails before the synthetic harness can run. Phase 04 deferred
// item 5 (real-engine Thymeleaf harness) is the structural fix.
// Java/Thymeleaf rides the Maven plumbing added in `prepare_java`:
// the harness ships a `pom.xml` via `extra_files`, prepare_java runs
// `mvn dependency:copy-dependencies -DoutputDirectory=lib` to stage
// `org.thymeleaf.*` jars, and javac compiles with `-cp .:lib/*`.
// The e2e cell SKIPs when `mvn` or `javac` is absent on the host.
mod e2e_phase_04 {
use crate::common::fixture_harness::FIXTURE_LOCK;
@ -355,7 +356,8 @@ mod e2e_phase_04 {
Lang::Ruby => "ruby",
Lang::Php => "php",
Lang::JavaScript => "node",
_ => unreachable!("e2e_phase_04 covers Python/Ruby/PHP/JS only"),
Lang::Java => "javac",
_ => unreachable!("e2e_phase_04 covers Python/Ruby/PHP/JS/Java only"),
}
}
@ -365,6 +367,7 @@ mod e2e_phase_04 {
Lang::Ruby => "ruby_erb",
Lang::Php => "php_twig",
Lang::JavaScript => "js_handlebars",
Lang::Java => "java_thymeleaf",
_ => unreachable!(),
}
}
@ -417,6 +420,12 @@ mod e2e_phase_04 {
eprintln!("SKIP {lang:?} {fixture}: missing toolchain {bin}");
return None;
}
// Java/Thymeleaf also needs Maven on PATH to resolve the
// Thymeleaf jars before javac runs.
if matches!(lang, Lang::Java) && !command_available("mvn") {
eprintln!("SKIP {lang:?} {fixture}: missing mvn for dependency resolution");
return None;
}
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let (spec, _tmp) = build_spec(lang, fixture, entry_name);
let opts = SandboxOptions {
@ -490,4 +499,18 @@ mod e2e_phase_04 {
.expect("Confirmed run must carry a DifferentialOutcome");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
#[test]
fn java_thymeleaf_vuln_confirms_via_run_spec() {
let Some(outcome) = run(Lang::Java, "vuln.java", "run") else { return };
assert!(
outcome.triggered_by.is_some(),
"Java Thymeleaf SSTI vuln must Confirm via run_spec; got {outcome:?}",
);
let diff = outcome
.differential
.as_ref()
.expect("Confirmed run must carry a DifferentialOutcome");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
}
}