mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0005 (20260520T233019Z-6958)
This commit is contained in:
parent
787da2975f
commit
c885a8d424
4 changed files with 150 additions and 21 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">`
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue