mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
refactor(dynamic): add recursive dependency resolution for SSA receivers, expand tests for Python and PHP
This commit is contained in:
parent
f49211d788
commit
baa9a36bc6
13 changed files with 329 additions and 76 deletions
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
use assert_cmd::Command;
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct Scenario {
|
||||
|
|
@ -43,13 +44,20 @@ fn fixture_root(rel: &str) -> PathBuf {
|
|||
.join(rel)
|
||||
}
|
||||
|
||||
fn run_scan_json(root: &PathBuf) -> Value {
|
||||
let assert = Command::cargo_bin("nyx")
|
||||
.expect("nyx binary")
|
||||
fn nyx_scan_cmd(home: &Path, root: &Path) -> Command {
|
||||
let mut cmd = Command::cargo_bin("nyx").expect("nyx binary");
|
||||
cmd.env("HOME", home)
|
||||
.env("XDG_CONFIG_HOME", home.join(".config"))
|
||||
.env("XDG_DATA_HOME", home.join(".local/share"))
|
||||
.env("NO_COLOR", "1")
|
||||
.args(["scan", "--format", "json"])
|
||||
.arg(root)
|
||||
.assert()
|
||||
.success();
|
||||
.arg(root);
|
||||
cmd
|
||||
}
|
||||
|
||||
fn run_scan_json(root: &Path) -> Value {
|
||||
let home = tempfile::tempdir().expect("temp home");
|
||||
let assert = nyx_scan_cmd(home.path(), root).assert().success();
|
||||
let stdout = String::from_utf8(assert.get_output().stdout.clone())
|
||||
.expect("nyx scan stdout is valid UTF-8");
|
||||
serde_json::from_str(&stdout).unwrap_or_else(|e| {
|
||||
|
|
@ -233,10 +241,8 @@ fn flask_eval_chain_replay_stable_honours_opt_in() {
|
|||
|
||||
// Arm 1: env var unset → replay_stable must be null on the top chain
|
||||
// regardless of verdict status.
|
||||
let assert_off = Command::cargo_bin("nyx")
|
||||
.expect("nyx binary")
|
||||
.args(["scan", "--format", "json"])
|
||||
.arg(&root)
|
||||
let home_off = tempfile::tempdir().expect("temp home");
|
||||
let assert_off = nyx_scan_cmd(home_off.path(), &root)
|
||||
.env_remove("NYX_VERIFY_REPLAY_STABLE")
|
||||
.assert()
|
||||
.success();
|
||||
|
|
@ -260,10 +266,8 @@ fn flask_eval_chain_replay_stable_honours_opt_in() {
|
|||
// verdict is Confirmed. When the toolchain is missing the verdict
|
||||
// stays Inconclusive and replay_stable stays null; both branches
|
||||
// are valid wiring outcomes.
|
||||
let assert_on = Command::cargo_bin("nyx")
|
||||
.expect("nyx binary")
|
||||
.args(["scan", "--format", "json"])
|
||||
.arg(&root)
|
||||
let home_on = tempfile::tempdir().expect("temp home");
|
||||
let assert_on = nyx_scan_cmd(home_on.path(), &root)
|
||||
.env("NYX_VERIFY_REPLAY_STABLE", "1")
|
||||
.assert()
|
||||
.success();
|
||||
|
|
@ -303,10 +307,9 @@ fn flask_eval_chain_replay_stable_honours_opt_in() {
|
|||
#[test]
|
||||
fn flask_eval_chain_dynamic_verdict_is_null_when_verify_disabled() {
|
||||
let root = fixture_root("python/flask_eval");
|
||||
let assert = Command::cargo_bin("nyx")
|
||||
.expect("nyx binary")
|
||||
.args(["scan", "--no-verify", "--format", "json"])
|
||||
.arg(&root)
|
||||
let home = tempfile::tempdir().expect("temp home");
|
||||
let assert = nyx_scan_cmd(home.path(), &root)
|
||||
.arg("--no-verify")
|
||||
.assert()
|
||||
.success();
|
||||
let stdout = String::from_utf8(assert.get_output().stdout.clone())
|
||||
|
|
|
|||
|
|
@ -162,6 +162,8 @@ fn class_method_python_dispatch_reads_payload_and_invokes_method() {
|
|||
assert!(h.source.contains("UserRepository"));
|
||||
assert!(h.source.contains("find_by_name"));
|
||||
assert!(h.source.contains("_nyx_build_receiver"));
|
||||
assert!(h.source.contains("depth=3"));
|
||||
assert!(h.source.contains("_nyx_resolve_annotation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -250,6 +252,17 @@ mod e2e_phase_19 {
|
|||
cap: Cap::SQL_QUERY,
|
||||
bins: &["python3"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Python,
|
||||
fixture_dir: "python_recursive_deps",
|
||||
vuln_file: "vuln.py",
|
||||
benign_file: "benign.py",
|
||||
vuln_class: "UserController",
|
||||
benign_class: "UserController",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["python3"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Ruby,
|
||||
fixture_dir: "ruby",
|
||||
|
|
@ -294,6 +307,17 @@ mod e2e_phase_19 {
|
|||
cap: Cap::CODE_EXEC,
|
||||
bins: &["php"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Php,
|
||||
fixture_dir: "php_recursive_deps",
|
||||
vuln_file: "vuln.php",
|
||||
benign_file: "benign.php",
|
||||
vuln_class: "UserController",
|
||||
benign_class: "UserController",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["php"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Java,
|
||||
fixture_dir: "java",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ mod dynamic_sandbox_cli {
|
|||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
fn scan_cmd_with_fresh_env() -> Command {
|
||||
fn scan_cmd_with_fresh_env() -> (tempfile::TempDir, Command) {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let mut cmd = Command::cargo_bin("nyx").expect("nyx binary");
|
||||
cmd.env("HOME", home.path())
|
||||
|
|
@ -20,13 +20,13 @@ mod dynamic_sandbox_cli {
|
|||
// Scan a non-existent path; the backend validation runs before any
|
||||
// filesystem work so the path doesn't need to exist for these tests.
|
||||
cmd.args(["scan", "/dev/null/nonexistent"]);
|
||||
cmd
|
||||
(home, cmd)
|
||||
}
|
||||
|
||||
/// `--unsafe-sandbox --backend docker` must be rejected with a clear error.
|
||||
#[test]
|
||||
fn unsafe_sandbox_with_docker_backend_is_rejected() {
|
||||
let mut cmd = scan_cmd_with_fresh_env();
|
||||
let (_home, mut cmd) = scan_cmd_with_fresh_env();
|
||||
cmd.args(["--unsafe-sandbox", "--backend", "docker"]);
|
||||
cmd.assert().failure().stderr(predicate::str::contains(
|
||||
"--unsafe-sandbox and --backend docker are mutually exclusive",
|
||||
|
|
@ -38,7 +38,7 @@ mod dynamic_sandbox_cli {
|
|||
/// no findings, etc.) but not with the mutex message.
|
||||
#[test]
|
||||
fn unsafe_sandbox_alone_does_not_trigger_mutex_error() {
|
||||
let mut cmd = scan_cmd_with_fresh_env();
|
||||
let (_home, mut cmd) = scan_cmd_with_fresh_env();
|
||||
cmd.arg("--unsafe-sandbox");
|
||||
cmd.assert().stderr(
|
||||
predicate::str::contains(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
// Benign control for recursive typed ClassMethod dependencies.
|
||||
|
||||
class Repository {
|
||||
private $dbConnection;
|
||||
|
||||
public function __construct($dbConnection) {
|
||||
$this->dbConnection = $dbConnection;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
class Service {
|
||||
private Repository $repository;
|
||||
|
||||
public function __construct(Repository $repository) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->repository->run($payload);
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
private Service $service;
|
||||
|
||||
public function __construct(Service $service) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->service->run($payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
// Class-method fixture with recursively constructed typed dependencies.
|
||||
|
||||
class Repository {
|
||||
private $dbConnection;
|
||||
|
||||
public function __construct($dbConnection) {
|
||||
$this->dbConnection = $dbConnection;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return shell_exec('true ' . $payload);
|
||||
}
|
||||
}
|
||||
|
||||
class Service {
|
||||
private Repository $repository;
|
||||
|
||||
public function __construct(Repository $repository) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->repository->run($payload);
|
||||
}
|
||||
}
|
||||
|
||||
class UserController {
|
||||
private Service $service;
|
||||
|
||||
public function __construct(Service $service) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function run($payload) {
|
||||
return $this->service->run($payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
"""Benign control for the recursive ClassMethod dependency fixture."""
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, db_connection):
|
||||
self._db = db_connection
|
||||
|
||||
def run(self, payload):
|
||||
return "ok"
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, repository: Repository):
|
||||
self._repository = repository
|
||||
|
||||
def run(self, payload):
|
||||
return self._repository.run(payload)
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, service: Service):
|
||||
self._service = service
|
||||
|
||||
def run(self, payload):
|
||||
return self._service.run(payload)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"""Class-method fixture with recursively constructed dependencies."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self, db_connection):
|
||||
self._db = db_connection
|
||||
|
||||
def run(self, payload):
|
||||
os.system(payload)
|
||||
|
||||
|
||||
class Service:
|
||||
def __init__(self, repository: Repository):
|
||||
self._repository = repository
|
||||
|
||||
def run(self, payload):
|
||||
self._repository.run(payload)
|
||||
|
||||
|
||||
class UserController:
|
||||
def __init__(self, service: Service):
|
||||
self._service = service
|
||||
|
||||
def run(self, payload):
|
||||
self._service.run(payload)
|
||||
|
|
@ -596,9 +596,14 @@ fn error_throw_terminates() {
|
|||
#[test]
|
||||
fn binary_json_output() {
|
||||
let fixture = fixture_path("rust_web_app");
|
||||
let home = tempfile::tempdir().expect("temp home");
|
||||
#[allow(deprecated)]
|
||||
let cmd = assert_cmd::Command::cargo_bin("nyx")
|
||||
.expect("nyx binary should exist")
|
||||
.env("HOME", home.path())
|
||||
.env("XDG_CONFIG_HOME", home.path().join(".config"))
|
||||
.env("XDG_DATA_HOME", home.path().join(".local/share"))
|
||||
.env("NO_COLOR", "1")
|
||||
.arg("scan")
|
||||
.arg(fixture.to_str().unwrap())
|
||||
.arg("--no-index")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue