[pitboss/grind] deferred session-0013 (20260522T043516Z-29b8)

This commit is contained in:
pitboss 2026-05-22 04:05:18 -05:00
parent 824a266303
commit 6f58921a17
4 changed files with 192 additions and 263 deletions

View file

@ -1354,112 +1354,18 @@ pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
} else {
spec.entry_name.clone()
};
let uses_real_xpath = entry_source.contains("javax.xml.xpath");
let main_body = if uses_real_xpath {
format!(
r#" // Phase 07 tier-(a): reflectively invoke the fixture's
// `run(String)` so the real `javax.xml.xpath.XPath.evaluate`
// call against the staged corpus document runs, then count
// the returned `NodeList` nodes. Falls back to the inline
// matcher when reflection fails so the harness still produces
// a verdict on a fixture whose `run` signature does not match.
int count = -1;
try {{
Class<?> entry = Class.forName("{entry_fqn}");
Method m = entry.getDeclaredMethod("{entry_method}", String.class);
m.setAccessible(true);
Object result = m.invoke(null, payload);
if (result instanceof NodeList) {{
count = ((NodeList) result).getLength();
}}
}} catch (ClassNotFoundException | NoSuchMethodException
| IllegalAccessException e) {{
// Fixture shape did not match (String) -> NodeList — fall
// through to the synthetic matcher below.
}} catch (InvocationTargetException ite) {{
// The fixture itself threw (malformed XPath, parse error,
// ...); treat as a 0-node return so a benign fixture that
// rejects the payload stays NotConfirmed.
count = 0;
}}
if (count < 0) {{
count = nyxXpathSelect(expr);
}}"#
)
} else {
r#" // No real javax.xml.xpath import on the entry source — fall
// back to the inline matcher path so the harness still
// produces a verdict.
int count = nyxXpathSelect(expr);"#
.to_owned()
};
let imports = if uses_real_xpath {
"import java.lang.reflect.InvocationTargetException;\n\
import java.lang.reflect.Method;\n\
import org.w3c.dom.NodeList;\n"
} else {
""
};
let source = format!(
r#"// Nyx dynamic harness — XPATH_INJECTION javax.xml.xpath.XPath.evaluate (Phase 07 / Track J.5).
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
{imports}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.w3c.dom.NodeList;
public class NyxHarness {{
{shim}
static final String[] NYX_XPATH_USERS = new String[] {{ "alice", "bob", "carol" }};
static int nyxXpathSelect(String expr) {{
String needle = "//user[@name=";
if (!expr.startsWith(needle)) return 0;
String rest = expr.substring(needle.length());
if (!rest.endsWith("]")) return 0;
String predicate = rest.substring(0, rest.length() - 1);
Matcher single = Pattern.compile("^'([^']*)'(.*)$").matcher(predicate);
if (single.find()) {{
String literal = single.group(1);
String tail = single.group(2).trim();
if (tail.isEmpty() || tail.equals("]")) {{
int count = 0;
for (String u : NYX_XPATH_USERS) if (u.equals(literal)) count++;
return count;
}}
if (Pattern.compile("^or\\s+", Pattern.CASE_INSENSITIVE).matcher(tail).find()) {{
return NYX_XPATH_USERS.length;
}}
}}
Matcher dbl = Pattern.compile("^\"([^\"]*)\"\\s*$").matcher(predicate);
if (dbl.find()) {{
String literal = dbl.group(1);
int count = 0;
for (String u : NYX_XPATH_USERS) if (u.equals(literal)) count++;
return count;
}}
if (Pattern.compile("^concat\\(", Pattern.CASE_INSENSITIVE).matcher(predicate).find()) {{
Matcher parts = Pattern.compile("'([^']*)'").matcher(predicate);
StringBuilder joined = new StringBuilder();
while (parts.find()) {{
String p = parts.group(1);
if (p.equals(",\"")) continue;
joined.append(p);
}}
String result = joined.toString().replace(",\"'\",", "'");
int count = 0;
for (String u : NYX_XPATH_USERS) if (u.equals(result)) count++;
return count;
}}
return NYX_XPATH_USERS.length;
}}
static void nyxXpathProbe(String expr, int nodesReturned) {{
String p = System.getenv("NYX_PROBE_PATH");
if (p == null || p.isEmpty()) return;
@ -1488,7 +1394,38 @@ public class NyxHarness {{
String payload = System.getenv("NYX_PAYLOAD");
if (payload == null) payload = "";
String expr = "//user[@name='" + payload + "']";
{main_body}
// Phase 07 tier-(a): reflectively invoke the fixture's
// `run(String)` so the real `javax.xml.xpath.XPath.evaluate`
// call against the staged corpus document runs, then count
// the returned `NodeList` nodes. Missing `javax.xml.xpath`
// / `org.w3c.dom` on the JDK is the only structural reason
// the reflective lookup fails; in that case we emit the
// conventional `NYX_IMPORT_ERROR:` stderr marker plus
// `System.exit(77)` so the runner maps the outcome to
// `RunError::BuildFailed` and the e2e SKIP branch fires.
int count;
try {{
Class<?> entry = Class.forName("{entry_fqn}");
Method m = entry.getDeclaredMethod("{entry_method}", String.class);
m.setAccessible(true);
Object result = m.invoke(null, payload);
if (result instanceof NodeList) {{
count = ((NodeList) result).getLength();
}} else {{
count = 0;
}}
}} catch (ClassNotFoundException | NoSuchMethodException
| IllegalAccessException e) {{
System.err.println("NYX_IMPORT_ERROR: " + e.getClass().getName() + ": " + e.getMessage());
System.exit(77);
return;
}} catch (InvocationTargetException ite) {{
// The fixture itself threw (malformed XPath, parse error,
// ...); treat as a 0-node return so a benign fixture that
// rejects the payload stays NotConfirmed.
count = 0;
}}
System.out.println("__NYX_XPATH_TIER_A__");
nyxXpathProbe(expr, count);
System.out.println("__NYX_SINK_HIT__");
StringBuilder body = new StringBuilder(64);
@ -3598,7 +3535,7 @@ mod tests {
}
#[test]
fn emit_xpath_harness_drives_fixture_through_real_xpath_when_imported() {
fn emit_xpath_harness_routes_through_real_xpath_reflectively() {
let dir = std::env::temp_dir().join("nyx_phase07_test_drive_fixture");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
@ -3634,15 +3571,16 @@ mod tests {
"tier-(a) harness must cast the result to NodeList and count nodes",
);
assert!(
h.source.contains("count = nyxXpathSelect(expr);"),
"tier-(a) harness must preserve the inline matcher as a fallback",
h.source.contains("__NYX_XPATH_TIER_A__"),
"tier-(a) harness must emit the tier-(a) stdout marker after the real reflective invoke: {}",
h.source
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_xpath_harness_falls_back_to_inline_matcher_without_xpath_import() {
let dir = std::env::temp_dir().join("nyx_phase07_test_no_xpath_import");
fn emit_xpath_harness_drops_inline_matcher_fallback() {
let dir = std::env::temp_dir().join("nyx_phase07_test_no_inline_matcher");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let entry = write_servlet_fixture(
@ -3655,16 +3593,27 @@ mod tests {
spec.entry_name = "run".into();
let h = emit_xpath_harness(&spec);
assert!(
!h.source.contains("import org.w3c.dom.NodeList;"),
"fallback path must not import NodeList",
!h.source.contains("nyxXpathSelect"),
"harness must not carry the inline `nyxXpathSelect` matcher; tier-(a) reflective invoke is the only path",
);
assert!(
!h.source.contains("Class.forName(\"Vuln\")"),
"fallback path must not invoke the fixture reflectively",
!h.source.contains("NYX_XPATH_USERS"),
"harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) reflective invoke is the only path",
);
assert!(
h.source.contains("int count = nyxXpathSelect(expr);"),
"fallback path must keep the inline matcher as the primary count source",
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("System.exit(77)"),
"harness must emit `NYX_IMPORT_ERROR:` stderr marker + `System.exit(77)` on reflective lookup failure: {}",
h.source
);
assert!(
h.source.contains("__NYX_XPATH_TIER_A__"),
"harness must emit the tier-(a) stdout marker: {}",
h.source
);
assert!(
h.source.contains("import org.w3c.dom.NodeList;")
&& h.source.contains("import java.lang.reflect.Method;"),
"harness must always import the reflective invocation path; the synthetic-only branch is gone",
);
}
}

View file

@ -1225,69 +1225,37 @@ pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
let shim = probe_shim();
let corpus_filename = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_FILENAME;
let corpus_xml = crate::dynamic::stubs::xpath_document::XPATH_CORPUS_XML;
let entry_source = read_entry_source(&spec.entry_file);
let entry_stem = derive_js_entry_stem(&spec.entry_file);
let entry_name = if spec.entry_name.is_empty() {
"run".to_owned()
} else {
spec.entry_name.clone()
};
let uses_real_xpath = entry_source.contains("require('xpath')")
|| entry_source.contains("require(\"xpath\")");
let body = format!(
r#"// Nyx dynamic harness — XPATH_INJECTION xpath.select (Phase 07 / Track J.5).
{shim}
const NYX_XPATH_USERS = ['alice', 'bob', 'carol'];
function nyxXpathSelect(expr) {{
const needle = "//user[@name=";
if (!expr.startsWith(needle)) return 0;
const rest = expr.slice(needle.length);
if (!rest.endsWith("]")) return 0;
const predicate = rest.slice(0, -1);
let m = predicate.match(/^'([^']*)'(.*)$/);
if (m) {{
const literal = m[1];
const tail = m[2].trim();
if (tail === '' || tail === ']') {{
return NYX_XPATH_USERS.filter((u) => u === literal).length;
}}
if (/^or\s+/i.test(tail)) {{
return NYX_XPATH_USERS.length;
}}
}}
m = predicate.match(/^"([^"]*)"\s*$/);
if (m) {{
const literal = m[1];
return NYX_XPATH_USERS.filter((u) => u === literal).length;
}}
if (/^concat\(/i.test(predicate)) {{
const parts = [...predicate.matchAll(/'([^']*)'/g)].map((x) => x[1]);
let joined = parts.filter((p) => p !== ',"').join('');
joined = joined.split(",\"'\",").join("'");
return NYX_XPATH_USERS.filter((u) => u === joined).length;
}}
return NYX_XPATH_USERS.length;
}}
function nyxXpathViaFixture(payload) {{
// Phase 07 tier-(a): require the fixture and call its
// `{entry_name}` so the real `xpath.select` (or other XPath evaluator
// the fixture chooses) runs against the staged corpus document.
// Returns the node count, or `null` when the require / lookup / call
// fails (e.g. the `xpath` npm package is not installed on the host)
// so the caller can fall back to the inline matcher.
// the fixture chooses) runs against the staged corpus document. A
// missing `xpath` host install is the only structural reason the
// require fails; in that case we emit the conventional
// `NYX_IMPORT_ERROR:` stderr marker plus `process.exit(77)` so the
// runner maps the outcome to `RunError::BuildFailed` and the e2e
// SKIP branch fires.
let _entry;
try {{
_entry = require('./{entry_stem}');
}} catch (e) {{
return null;
process.stderr.write('NYX_IMPORT_ERROR: ' + e.message + '\n');
process.exit(77);
}}
const fn = _entry && (typeof _entry === 'function' ? _entry : _entry['{entry_name}']);
if (typeof fn !== 'function') return null;
if (typeof fn !== 'function') {{
throw new Error("Phase 07 XPath harness: entry function '{entry_name}' not found in fixture module './{entry_stem}'");
}}
let result;
try {{
result = fn(payload);
@ -1321,23 +1289,21 @@ function nyxXpathProbe(expr, nodesReturned) {{
const payload = process.env.NYX_PAYLOAD || '';
const expr = "//user[@name='" + payload + "']";
let nodes = nyxXpathViaFixture(payload);
if (nodes === null) {{
nodes = nyxXpathSelect(expr);
}}
const nodes = nyxXpathViaFixture(payload);
console.log('__NYX_XPATH_TIER_A__');
nyxXpathProbe(expr, nodes);
console.log('__NYX_SINK_HIT__');
console.log(JSON.stringify({{ expr: expr, nodes_returned: nodes }}));
"#
);
let mut extra_files = vec![(corpus_filename.to_owned(), corpus_xml.to_owned())];
if uses_real_xpath {
extra_files.push(("package.json".to_owned(), package_json_xpath()));
extra_files.push((
let extra_files = vec![
(corpus_filename.to_owned(), corpus_xml.to_owned()),
("package.json".to_owned(), package_json_xpath()),
(
"package-lock.json".to_owned(),
package_lock_skeleton("nyx-harness-xpath"),
));
}
),
];
HarnessSource {
source: body,
filename: "harness.js".to_owned(),
@ -2857,7 +2823,7 @@ mod tests {
}
#[test]
fn emit_xpath_harness_drives_fixture_through_real_xpath_when_imported() {
fn emit_xpath_harness_routes_through_fixture_require() {
let dir = std::env::temp_dir().join("nyx_phase07_js_test_drive_fixture");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
@ -2891,34 +2857,34 @@ mod tests {
h.source
);
assert!(
h.source.contains("nodes = nyxXpathSelect(expr);"),
"tier-(a) harness must preserve the inline matcher as a fallback: {}",
h.source.contains("__NYX_XPATH_TIER_A__"),
"harness must emit the tier-(a) stdout marker after the real xpath call: {}",
h.source
);
assert!(
h.extra_files
.iter()
.any(|(p, c)| p == "package.json" && c.contains("\"xpath\"")),
"tier-(a) harness must stage a package.json with the xpath dep",
"harness must always stage a package.json with the xpath dep",
);
assert!(
h.extra_files
.iter()
.any(|(p, c)| p == "package.json" && c.contains("@xmldom/xmldom")),
"tier-(a) harness must stage a package.json with the xmldom dep",
"harness must always stage a package.json with the xmldom dep",
);
assert!(
h.extra_files
.iter()
.any(|(p, _)| p == "package-lock.json"),
"tier-(a) harness must stage a package-lock.json",
"harness must always stage a package-lock.json",
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn emit_xpath_harness_falls_back_to_inline_matcher_without_xpath_require() {
let dir = std::env::temp_dir().join("nyx_phase07_js_test_no_xpath_require");
fn emit_xpath_harness_drops_inline_matcher_fallback() {
let dir = std::env::temp_dir().join("nyx_phase07_js_test_no_inline_matcher");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let entry = dir.join("vuln.js");
@ -2929,14 +2895,28 @@ mod tests {
.unwrap();
let h = emit_xpath_harness(&make_xpath_spec(entry.to_str().unwrap(), "run"));
assert!(
!h.extra_files.iter().any(|(p, _)| p == "package.json"),
"fallback path must not stage a package.json (xpath dep would be unused)",
!h.source.contains("nyxXpathSelect"),
"harness must not carry the inline `nyxXpathSelect` matcher; tier-(a) is the only path",
);
assert!(
!h.extra_files
!h.source.contains("NYX_XPATH_USERS"),
"harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) is the only path",
);
assert!(
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("process.exit(77)"),
"harness must emit `NYX_IMPORT_ERROR:` stderr marker + `process.exit(77)` on require failure: {}",
h.source
);
assert!(
h.source.contains("__NYX_XPATH_TIER_A__"),
"harness must emit the tier-(a) stdout marker: {}",
h.source
);
assert!(
h.extra_files
.iter()
.any(|(p, _)| p == "package-lock.json"),
"fallback path must not stage a package-lock.json",
.any(|(p, _)| p == "package.json"),
"harness must always stage a package.json (real-xpath dep is required, no synthetic-only path)",
);
let _ = std::fs::remove_dir_all(&dir);
}

View file

@ -1230,77 +1230,6 @@ pub fn emit_xpath_harness(spec: &HarnessSpec) -> HarnessSource {
// Nyx dynamic harness — XPATH_INJECTION DOMXPath::query (Phase 07 / Track J.5).
{shim}
// Synthetic in-process XPath evaluator over the canonical staged
// document — counts <user> nodes that satisfy the `[@name='…']`
// predicate the host code synthesised from the payload. Real
// `DOMXPath::query` is not invoked (the harness ignores `_spec` and
// inlines the evaluator); the differential rule still holds because
// the vuln payload's `' or '1'='1` tail rewraps the selector into a
// match-everything shape.
$NYX_XPATH_USERS = ['alice', 'bob', 'carol'];
function _nyx_xpath_select($expr, array $users): int {{
// Recognise the canonical `//user[@name='<payload>']` shape the
// synthetic harness emits. Anything else falls through to "no
// match" so a malformed expression cannot accidentally confirm.
$needle = "//user[@name=";
if (strncmp($expr, $needle, strlen($needle)) !== 0) {{
return 0;
}}
$rest = substr($expr, strlen($needle));
if (!str_ends_with($rest, ']')) {{
return 0;
}}
$predicate = substr($rest, 0, strlen($rest) - 1);
if (preg_match("/^'([^']*)'(.*)\$/", $predicate, $m)) {{
// `name='alice'` → exact-match against the literal
// `name='alice' or '1'='1'` → OR-tail breakouts; presence of
// ` or ` after the closing quote means the selector is now
// tautological → every user matches.
$literal = $m[1];
$tail = trim($m[2]);
if ($tail === '' || $tail === ']') {{
$count = 0;
foreach ($users as $u) {{
if ($u === $literal) $count++;
}}
return $count;
}}
if (preg_match("/^or\\s+/i", $tail)) {{
return count($users);
}}
}}
if (preg_match('/^"([^"]*)"\\s*$/', $predicate, $m)) {{
$literal = $m[1];
$count = 0;
foreach ($users as $u) {{
if ($u === $literal) $count++;
}}
return $count;
}}
if (preg_match("/^concat\\(/i", $predicate)) {{
// `concat('a',\"'\",'b')` benign-escape path: extract the
// joined literal and match exactly once.
if (preg_match_all("/'([^']*)'/", $predicate, $parts)) {{
$joined = '';
foreach ($parts[1] as $p) {{
if ($p === ',"') continue;
$joined .= $p;
}}
// Normalise embedded single-quote literals back to the
// raw character so a `concat`-quoted username collapses
// to the same literal the user typed.
$joined = str_replace(",\"'\",", "'", $joined);
$count = 0;
foreach ($users as $u) {{
if ($u === $joined) $count++;
}}
return $count;
}}
}}
return count($users);
}}
function _nyx_xpath_probe(string $expr, int $nodes_returned): void {{
$p = getenv('NYX_PROBE_PATH');
if ($p === false || $p === '') return;
@ -1315,23 +1244,34 @@ function _nyx_xpath_probe(string $expr, int $nodes_returned): void {{
@file_put_contents($p, json_encode($rec) . "\n", FILE_APPEND);
}}
function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string $entry_name): ?int {{
function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string $entry_name): int {{
// Phase 07 tier-(a): require the fixture file and call its
// `$entry_name` function so the real `DOMXPath::query` runs
// against the staged corpus document. Returns the result-set
// length, or `null` when the require / call fails so the caller
// can fall back to the inline matcher.
// against the staged corpus document. A missing `ext-dom` /
// `ext-xml` host install or an inaccessible fixture file is the
// only structural reason this fails; in that case we emit the
// conventional `NYX_IMPORT_ERROR:` stderr marker plus `exit(77)`
// so the runner maps the outcome to `RunError::BuildFailed` and
// the e2e SKIP branch fires.
if (!class_exists('DOMDocument') || !class_exists('DOMXPath')) {{
fwrite(STDERR, "NYX_IMPORT_ERROR: ext-dom / ext-xml not loaded\n");
exit(77);
}}
$candidate = __DIR__ . DIRECTORY_SEPARATOR . $entry_basename;
if (!is_file($candidate)) {{
return null;
fwrite(STDERR, "NYX_IMPORT_ERROR: fixture file not found at $candidate\n");
exit(77);
}}
try {{
require_once $candidate;
}} catch (\Throwable $_) {{
return null;
}} catch (\Throwable $_e) {{
fwrite(STDERR, "NYX_IMPORT_ERROR: " . $_e->getMessage() . "\n");
exit(77);
}}
if (!function_exists($entry_name)) {{
return null;
throw new \RuntimeException(
"Phase 07 XPath harness: entry function '$entry_name' not found in fixture '$entry_basename'"
);
}}
try {{
$result = $entry_name($payload);
@ -1346,15 +1286,13 @@ function _nyx_xpath_via_fixture(string $payload, string $entry_basename, string
if (is_array($result)) {{
return count($result);
}}
return null;
return 0;
}}
$payload = (string) (getenv('NYX_PAYLOAD') ?: '');
$expr = "//user[@name='" . $payload . "']";
$nodes = _nyx_xpath_via_fixture($payload, "{entry_basename}", "{entry_name}");
if ($nodes === null) {{
$nodes = _nyx_xpath_select($expr, $NYX_XPATH_USERS);
}}
echo "__NYX_XPATH_TIER_A__\n";
_nyx_xpath_probe($expr, $nodes);
echo "__NYX_SINK_HIT__\n";
echo json_encode(['expr' => $expr, 'nodes_returned' => $nodes]) . "\n";
@ -2494,9 +2432,35 @@ mod tests {
"PHP XPath harness must check the result against DOMNodeList",
);
assert!(
h.source.contains("__NYX_XPATH_TIER_A__"),
"PHP XPath harness must emit the tier-(a) stdout marker after the real DOMXPath call: {}",
h.source
);
}
#[test]
fn emit_xpath_harness_drops_inline_matcher_fallback() {
let h = emit_xpath_harness(&make_xpath_spec(
"tests/dynamic_fixtures/xpath_injection/php/vuln.php",
"run",
));
assert!(
!h.source.contains("_nyx_xpath_select"),
"PHP XPath harness must not carry the inline `_nyx_xpath_select` matcher; tier-(a) is the only path",
);
assert!(
!h.source.contains("NYX_XPATH_USERS"),
"PHP XPath harness must not carry the inline `NYX_XPATH_USERS` table; tier-(a) is the only path",
);
assert!(
h.source.contains("NYX_IMPORT_ERROR:") && h.source.contains("exit(77)"),
"PHP XPath harness must emit `NYX_IMPORT_ERROR:` stderr marker + `exit(77)` on require / ext failure: {}",
h.source
);
assert!(
h.source.contains("__NYX_XPATH_TIER_A__"),
"PHP XPath harness must emit the tier-(a) stdout marker: {}",
h.source
.contains("$nodes = _nyx_xpath_select($expr, $NYX_XPATH_USERS);"),
"PHP XPath harness must keep the inline matcher as a fallback",
);
}

View file

@ -403,8 +403,11 @@ fn staged_corpus_carries_three_users() {
// verdict path is deterministic without spawning a real XPath
// engine (`stubs_required: vec![]`).
//
// JavaScript is skipped: the synthetic harness's `require('xpath')`
// import resolves only when the workdir has the package installed.
// Each lang asserts the tier-(a) stdout marker so a regression that
// silently falls back to the inline matcher (now deleted) trips the
// test; on hosts without the real engine installed the harness exits
// 77 with `NYX_IMPORT_ERROR:` and `is_runtime_import_error` maps it to
// `RunError::BuildFailed` (SKIP).
mod e2e_phase_07 {
use crate::common::fixture_harness::FIXTURE_LOCK;
@ -533,6 +536,17 @@ mod e2e_phase_07 {
.as_ref()
.expect("Confirmed run must carry a DifferentialOutcome");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
let saw_tier_a = outcome.attempts.iter().any(|a| {
a.outcome
.stdout
.windows(tier_a_marker.len())
.any(|w| w == tier_a_marker)
});
assert!(
saw_tier_a,
"Java XPath vuln must reach the tier-(a) real-javax.xml.xpath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `nyxXpathSelect` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + System.exit(77) when the reflective lookup fails",
);
}
#[test]
@ -576,6 +590,17 @@ mod e2e_phase_07 {
.as_ref()
.expect("Confirmed run must carry a DifferentialOutcome");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
let saw_tier_a = outcome.attempts.iter().any(|a| {
a.outcome
.stdout
.windows(tier_a_marker.len())
.any(|w| w == tier_a_marker)
});
assert!(
saw_tier_a,
"PHP XPath vuln must reach the tier-(a) real-DOMXPath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `_nyx_xpath_select` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + exit 77 when ext-dom/ext-xml is unavailable",
);
}
#[test]
@ -592,5 +617,16 @@ mod e2e_phase_07 {
.as_ref()
.expect("Confirmed run must carry a DifferentialOutcome");
assert_eq!(diff.verdict, DifferentialVerdict::Confirmed);
let tier_a_marker = b"__NYX_XPATH_TIER_A__";
let saw_tier_a = outcome.attempts.iter().any(|a| {
a.outcome
.stdout
.windows(tier_a_marker.len())
.any(|w| w == tier_a_marker)
});
assert!(
saw_tier_a,
"JavaScript XPath vuln must reach the tier-(a) real-xpath path (stdout marker `__NYX_XPATH_TIER_A__`); the inline `nyxXpathSelect` fallback was removed and the harness now SKIPs via NYX_IMPORT_ERROR + exit 77 when the `xpath` npm package is unavailable",
);
}
}