mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +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
|
|
@ -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