nyx/tests/javascript_fixtures.rs

279 lines
9.8 KiB
Rust
Raw Normal View History

//! JavaScript per-shape acceptance tests (Phase 13 — Track B JS / TS vertical).
//!
//! For each [`nyx_scanner::dynamic::lang::js_shared::JsShape`] this suite
//! asserts:
//!
//! 1. The vuln fixture confirms (cmdi / xss oracle fires on the process
//! backend, sink probe lights up).
//! 2. The benign fixture does NOT confirm.
//!
//! Framework-bound shapes (Express / Koa / Next.js / browser-event under
//! jsdom) skip with an `eprintln!` when the package is unimportable in the
//! host's `node` interpreter — `prepare_node`'s `npm install --no-save`
//! would otherwise hang on a clean offline CI environment. In a developer
//! workstation with the framework installed globally / via the lockfile,
//! the test attempts the full pipeline.
mod common;
#[cfg(feature = "dynamic")]
mod javascript_fixture_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 node_available() -> bool {
std::process::Command::new("node")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn node_module_available(name: &'static str) -> bool {
std::process::Command::new("node")
.arg("-e")
.arg(format!("require.resolve('{name}')"))
.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::JavaScript,
"javascript",
shape,
file,
func,
cap,
sink_line,
kind,
slot,
)
}
// ── commonjs_export ─────────────────────────────────────────────────────
#[test]
fn commonjs_export_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"commonjs_export", "vuln.js", "runPing", Cap::CODE_EXEC, 11,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_confirmed("commonjs_export", &r);
}
#[test]
fn commonjs_export_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"commonjs_export", "benign.js", "runPing", Cap::CODE_EXEC, 11,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_not_confirmed("commonjs_export", &r);
}
// ── async_function ──────────────────────────────────────────────────────
#[test]
fn async_function_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"async_function", "vuln.js", "runPing", Cap::CODE_EXEC, 15,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_confirmed("async_function", &r);
}
#[test]
fn async_function_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"async_function", "benign.js", "runPing", Cap::CODE_EXEC, 14,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_not_confirmed("async_function", &r);
}
// ── esm_default ─────────────────────────────────────────────────────────
#[test]
fn esm_default_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"esm_default", "vuln.js", "runPing", Cap::CODE_EXEC, 14,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_confirmed("esm_default", &r);
}
#[test]
fn esm_default_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
let r = run(
"esm_default", "benign.js", "runPing", Cap::CODE_EXEC, 14,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_not_confirmed("esm_default", &r);
}
// ── express (framework-bound) ───────────────────────────────────────────
#[test]
fn express_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("express") {
eprintln!("SKIP: express not importable");
return;
}
let r = run(
"express", "vuln.js", "ping", Cap::CODE_EXEC, 15,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_confirmed("express", &r);
}
#[test]
fn express_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("express") {
eprintln!("SKIP: express not importable");
return;
}
let r = run(
"express", "benign.js", "ping", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_not_confirmed("express", &r);
}
// ── koa (framework-bound) ───────────────────────────────────────────────
#[test]
fn koa_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("koa") {
eprintln!("SKIP: koa not importable");
return;
}
let r = run(
"koa", "vuln.js", "ping", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_confirmed("koa", &r);
}
#[test]
fn koa_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("koa") {
eprintln!("SKIP: koa not importable");
return;
}
let r = run(
"koa", "benign.js", "ping", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_not_confirmed("koa", &r);
}
// ── next_route (framework-bound) ────────────────────────────────────────
#[test]
fn next_route_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("next") {
eprintln!("SKIP: next not importable");
return;
}
let r = run(
"next_route", "vuln.js", "handler", Cap::CODE_EXEC, 17,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_confirmed("next_route", &r);
}
#[test]
fn next_route_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("next") {
eprintln!("SKIP: next not importable");
return;
}
let r = run(
"next_route", "benign.js", "handler", Cap::CODE_EXEC, 14,
EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()),
);
assert_not_confirmed("next_route", &r);
}
// ── browser_event (jsdom) ───────────────────────────────────────────────
#[test]
fn browser_event_vuln_is_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("jsdom") {
eprintln!("SKIP: jsdom not importable");
return;
}
let r = run(
"browser_event", "vuln.js", "clickHandler", Cap::HTML_ESCAPE, 14,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_confirmed("browser_event", &r);
}
#[test]
fn browser_event_benign_not_confirmed() {
if !node_available() { eprintln!("SKIP: node not available"); return; }
if !node_module_available("jsdom") {
eprintln!("SKIP: jsdom not importable");
return;
}
let r = run(
"browser_event", "benign.js", "clickHandler", Cap::HTML_ESCAPE, 14,
EntryKind::Function, PayloadSlot::Param(0),
);
assert_not_confirmed("browser_event", &r);
}
}