mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
217 lines
9.3 KiB
Rust
217 lines
9.3 KiB
Rust
//! Phase 15 (Track L.13) — Ruby framework adapter integration tests.
|
|
//!
|
|
//! Each test exercises `detect_binding` end-to-end against a fixture
|
|
//! file under `tests/dynamic_fixtures/ruby/`, asserting that the
|
|
//! right adapter fires, the binding carries
|
|
//! `EntryKind::HttpRoute`, and the `RouteShape` matches the brief's
|
|
//! contract. Benign fixtures must produce the same adapter binding
|
|
//! shape as the vuln fixtures — the adapter only models the route,
|
|
//! the differential outcome of a verifier run is what distinguishes
|
|
//! the two.
|
|
|
|
#![cfg(feature = "dynamic")]
|
|
|
|
use nyx_scanner::dynamic::framework::{
|
|
FrameworkDetectionContext, HttpMethod, ParamSource, ProjectFileIndex, detect_binding,
|
|
detect_binding_with_project_context,
|
|
};
|
|
use nyx_scanner::evidence::EntryKind;
|
|
use nyx_scanner::summary::FuncSummary;
|
|
use nyx_scanner::symbol::Lang;
|
|
|
|
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
|
|
parser.set_language(&lang).unwrap();
|
|
parser.parse(src, None).unwrap()
|
|
}
|
|
|
|
fn summary_for(name: &str, file: &str) -> FuncSummary {
|
|
FuncSummary {
|
|
name: name.into(),
|
|
file_path: file.into(),
|
|
lang: "ruby".into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
// ── Rails ────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn rails_vuln_fixture_binds_route() {
|
|
let path = "tests/dynamic_fixtures/ruby/rails_action/vuln.rb";
|
|
let bytes = std::fs::read(path).expect("rails vuln fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("index", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("rails adapter must bind");
|
|
assert_eq!(binding.adapter, "ruby-rails");
|
|
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
assert_eq!(route.path, "/index");
|
|
}
|
|
|
|
#[test]
|
|
fn rails_benign_fixture_binds_same_route_shape() {
|
|
let path = "tests/dynamic_fixtures/ruby/rails_action/benign.rb";
|
|
let bytes = std::fs::read(path).expect("rails benign fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("index", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("rails adapter must bind benign fixture");
|
|
assert_eq!(binding.adapter, "ruby-rails");
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.path, "/index");
|
|
}
|
|
|
|
#[test]
|
|
fn rails_routes_draw_overrides_default_path() {
|
|
let src: &[u8] = b"Rails.application.routes.draw do\n get '/run', to: 'users#index'\nend\n\nclass UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n";
|
|
let tree = parse_ruby(src);
|
|
let summary = summary_for("index", "synth.rb");
|
|
let binding = detect_binding(&summary, tree.root_node(), src, Lang::Ruby)
|
|
.expect("rails adapter must bind via routes.draw");
|
|
assert_eq!(binding.adapter, "ruby-rails");
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.path, "/run");
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
}
|
|
|
|
// ── Sinatra ──────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn sinatra_vuln_fixture_binds_route() {
|
|
let path = "tests/dynamic_fixtures/ruby/sinatra_route/vuln.rb";
|
|
let bytes = std::fs::read(path).expect("sinatra vuln fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("run", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("sinatra adapter must bind");
|
|
assert_eq!(binding.adapter, "ruby-sinatra");
|
|
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
assert_eq!(route.path, "/run/:payload");
|
|
let payload_binding = binding
|
|
.request_params
|
|
.iter()
|
|
.find(|p| p.name == "payload")
|
|
.expect("payload path param");
|
|
assert!(matches!(
|
|
payload_binding.source,
|
|
ParamSource::PathSegment(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn sinatra_benign_fixture_binds_same_route_shape() {
|
|
let path = "tests/dynamic_fixtures/ruby/sinatra_route/benign.rb";
|
|
let bytes = std::fs::read(path).expect("sinatra benign fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("run", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("sinatra adapter must bind benign fixture");
|
|
assert_eq!(binding.adapter, "ruby-sinatra");
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.path, "/run/:payload");
|
|
}
|
|
|
|
// ── Hanami ───────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn hanami_vuln_fixture_binds_route() {
|
|
let path = "tests/dynamic_fixtures/ruby/hanami_action/vuln.rb";
|
|
let bytes = std::fs::read(path).expect("hanami vuln fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("call", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("hanami adapter must bind");
|
|
assert_eq!(binding.adapter, "ruby-hanami");
|
|
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
assert_eq!(route.path, "/run");
|
|
let req_binding = binding
|
|
.request_params
|
|
.iter()
|
|
.find(|p| p.name == "req")
|
|
.expect("req formal");
|
|
assert!(matches!(req_binding.source, ParamSource::Implicit));
|
|
}
|
|
|
|
#[test]
|
|
fn hanami_benign_fixture_binds_same_route_shape() {
|
|
let path = "tests/dynamic_fixtures/ruby/hanami_action/benign.rb";
|
|
let bytes = std::fs::read(path).expect("hanami benign fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("call", path);
|
|
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby)
|
|
.expect("hanami adapter must bind benign fixture");
|
|
assert_eq!(binding.adapter, "ruby-hanami");
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.path, "/run");
|
|
}
|
|
|
|
#[test]
|
|
fn hanami_config_routes_fixture_binds_cross_file_route() {
|
|
let path = "tests/dynamic_fixtures/ruby/hanami_config_routes/app/actions/books/show.rb";
|
|
let routes = "tests/dynamic_fixtures/ruby/hanami_config_routes/config/routes.rb";
|
|
let bytes = std::fs::read(path).expect("hanami action fixture exists");
|
|
let route_bytes = std::fs::read(routes).expect("hanami routes fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("call", path);
|
|
let mut project_files = ProjectFileIndex::new();
|
|
project_files.insert("config/routes.rb", route_bytes);
|
|
let context = FrameworkDetectionContext {
|
|
ssa_summary: None,
|
|
project_files: &project_files,
|
|
};
|
|
let binding = detect_binding_with_project_context(
|
|
&summary,
|
|
context,
|
|
tree.root_node(),
|
|
&bytes,
|
|
Lang::Ruby,
|
|
)
|
|
.expect("hanami adapter must bind through config/routes.rb");
|
|
assert_eq!(binding.adapter, "ruby-hanami");
|
|
let route = binding.route.as_ref().expect("route");
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
assert_eq!(route.path, "/books/:id");
|
|
}
|
|
|
|
// ── Cross-adapter disambiguation ─────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn sinatra_does_not_fire_on_rails_controller() {
|
|
let path = "tests/dynamic_fixtures/ruby/rails_action/vuln.rb";
|
|
let bytes = std::fs::read(path).expect("rails vuln fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("index", path);
|
|
let binding =
|
|
detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby).expect("adapter binds");
|
|
// First-match-wins ordering must produce `ruby-rails`, not
|
|
// `ruby-sinatra`, even if both adapters could in theory match.
|
|
assert_eq!(binding.adapter, "ruby-rails");
|
|
}
|
|
|
|
#[test]
|
|
fn hanami_does_not_fire_on_plain_class_with_call_method() {
|
|
let path = "tests/dynamic_fixtures/ruby/rack_middleware/vuln.rb";
|
|
let bytes = std::fs::read(path).expect("rack vuln fixture exists");
|
|
let tree = parse_ruby(&bytes);
|
|
let summary = summary_for("call", path);
|
|
let binding_opt = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby);
|
|
// The rack_middleware fixture has no Hanami::Action import or
|
|
// superclass; Hanami must not claim it. No other Phase 15 route
|
|
// adapter matches either (no Rails / Sinatra markers), so binding
|
|
// is `None` overall for the Phase 15 route slice. Sink adapters
|
|
// (header-ruby / redirect-ruby / etc.) also do not fire because
|
|
// the rack fixture's callees are not redirect / header sinks.
|
|
if let Some(b) = binding_opt {
|
|
assert_ne!(b.adapter, "ruby-hanami");
|
|
assert_ne!(b.adapter, "ruby-rails");
|
|
assert_ne!(b.adapter, "ruby-sinatra");
|
|
}
|
|
}
|