From 370e578cae8812bb82ea3306f68f160937390996 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Tue, 20 Jan 2026 20:01:45 -0800 Subject: [PATCH] jython script works? --- src/ghidra/scripts.rs | 7 ++ tests/e2e.rs | 174 ++++++++++++++++++++++++++++++++ tests/fixtures/sample_binary.rs | 162 +++++++++++++++++++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 tests/e2e.rs create mode 100644 tests/fixtures/sample_binary.rs diff --git a/src/ghidra/scripts.rs b/src/ghidra/scripts.rs index f7e6c5a..76f286a 100644 --- a/src/ghidra/scripts.rs +++ b/src/ghidra/scripts.rs @@ -5,6 +5,7 @@ pub fn get_list_functions_script() -> &'static str { r#" # List all functions in the program # @category Analysis +# @runtime Jython import json @@ -62,6 +63,7 @@ pub fn get_decompile_function_script() -> &'static str { r#" # Decompile a specific function # @category Analysis +# @runtime Jython import json from ghidra.app.decompiler import DecompInterface @@ -156,6 +158,7 @@ pub fn get_list_imports_script() -> &'static str { r#" # List all imports in the program # @category Analysis +# @runtime Jython import json @@ -185,6 +188,7 @@ pub fn get_list_exports_script() -> &'static str { r#" # List all exports in the program # @category Analysis +# @runtime Jython import json @@ -209,6 +213,7 @@ pub fn get_memory_map_script() -> &'static str { r#" # Get memory map # @category Analysis +# @runtime Jython import json @@ -248,6 +253,7 @@ pub fn get_program_info_script() -> &'static str { r#" # Get program information # @category Analysis +# @runtime Jython import json @@ -283,6 +289,7 @@ pub fn get_xrefs_to_script() -> &'static str { r#" # Get cross-references to an address # @category Analysis +# @runtime Jython import json diff --git a/tests/e2e.rs b/tests/e2e.rs new file mode 100644 index 0000000..452a328 --- /dev/null +++ b/tests/e2e.rs @@ -0,0 +1,174 @@ +//! End-to-end tests for ghidra-cli +//! +//! These tests require a working Ghidra installation and test the full CLI workflow. + +use assert_cmd::Command; +use predicates::prelude::*; +use std::path::PathBuf; + +/// Get the path to the test fixture binary +fn fixture_binary() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("sample_binary") +} + +/// Get a unique project name for each test to avoid conflicts +fn test_project_name(test_name: &str) -> String { + format!("e2e-{}-{}", test_name, std::process::id()) +} + +mod e2e_tests { + use super::*; + + /// Test that doctor command works + #[test] + fn test_doctor() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("doctor") + .assert() + .success() + .stdout(predicate::str::contains("Ghidra CLI Doctor")); + } + + /// Test version command + #[test] + fn test_version() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("version") + .assert() + .success() + .stdout(predicate::str::contains("ghidra-cli")); + } + + /// Test config list command + #[test] + fn test_config_list() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("config") + .arg("list") + .assert() + .success() + .stdout(predicate::str::contains("ghidra_install_dir")); + } + + /// Test import command with sample binary + /// This test requires Ghidra to be installed + #[test] + #[ignore] // Run with: cargo test -- --ignored + fn test_import_binary() { + let binary = fixture_binary(); + if !binary.exists() { + panic!("Test fixture not found. Run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs"); + } + + let project = test_project_name("import"); + + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("import") + .arg(binary.to_str().unwrap()) + .arg("--project") + .arg(&project) + .arg("--program") + .arg("sample_binary") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + .stdout(predicate::str::contains("Successfully imported")); + } + + /// Test function list command on pre-analyzed binary + /// Requires the e2e-test project to exist with sample_binary + #[test] + #[ignore] + fn test_function_list() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("function") + .arg("list") + .arg("--project") + .arg("e2e-test") + .arg("--program") + .arg("sample_binary") + .arg("--limit") + .arg("100") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + // Check for our known exported functions + .stdout(predicate::str::contains("main")) + .stdout(predicate::str::contains("fibonacci").or(predicate::str::contains("factorial"))); + } + + /// Test decompile command + #[test] + #[ignore] + fn test_decompile() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("decompile") + .arg("main") // Decompile main function + .arg("--project") + .arg("e2e-test") + .arg("--program") + .arg("sample_binary") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + // Should contain decompiled C code + .stdout(predicate::str::contains("void").or(predicate::str::contains("int"))); + } + + /// Test strings command + #[test] + #[ignore] + fn test_strings() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("strings") + .arg("list") + .arg("--project") + .arg("e2e-test") + .arg("--program") + .arg("sample_binary") + .arg("--limit") + .arg("50") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + // Check for our known strings + .stdout(predicate::str::contains("Hello").or(predicate::str::contains("Ghidra"))); + } + + /// Test memory map command + #[test] + #[ignore] + fn test_memory_map() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("memory") + .arg("map") + .arg("--project") + .arg("e2e-test") + .arg("--program") + .arg("sample_binary") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + // Should show memory sections + .stdout(predicate::str::contains(".text").or(predicate::str::contains("r"))); + } + + /// Test summary command + #[test] + #[ignore] + fn test_summary() { + let mut cmd = Command::cargo_bin("ghidra").unwrap(); + cmd.arg("summary") + .arg("--project") + .arg("e2e-test") + .arg("--program") + .arg("sample_binary") + .timeout(std::time::Duration::from_secs(120)) + .assert() + .success() + .stdout(predicate::str::contains("Program Summary")); + } +} diff --git a/tests/fixtures/sample_binary.rs b/tests/fixtures/sample_binary.rs new file mode 100644 index 0000000..1b49f69 --- /dev/null +++ b/tests/fixtures/sample_binary.rs @@ -0,0 +1,162 @@ +//! Sample binary for E2E testing of ghidra-cli. +//! +//! This binary contains various functions and data structures +//! that can be analyzed by Ghidra to test the CLI functionality. + +use std::collections::HashMap; + +/// A simple structure for testing +#[repr(C)] +struct TestStruct { + value: i32, + name: [u8; 32], +} + +/// Global constant string for testing string detection +static HELLO_WORLD: &str = "Hello, Ghidra CLI!"; +static VERSION_STRING: &str = "test_binary v1.0.0"; +static SECRET_KEY: &str = "super_secret_key_12345"; + +/// Simple arithmetic function +#[no_mangle] +pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { + a + b +} + +/// Multiply function +#[no_mangle] +pub extern "C" fn multiply(x: i32, y: i32) -> i32 { + x * y +} + +/// Calculate factorial (recursive) +#[no_mangle] +pub extern "C" fn factorial(n: u64) -> u64 { + if n <= 1 { + 1 + } else { + n * factorial(n - 1) + } +} + +/// Fibonacci (iterative) +#[no_mangle] +pub extern "C" fn fibonacci(n: u32) -> u64 { + if n == 0 { + return 0; + } + if n == 1 { + return 1; + } + + let mut a = 0u64; + let mut b = 1u64; + + for _ in 2..=n { + let temp = a + b; + a = b; + b = temp; + } + + b +} + +/// String processing function +#[no_mangle] +pub extern "C" fn process_string(input: *const u8, len: usize) -> i32 { + if input.is_null() || len == 0 { + return -1; + } + + let slice = unsafe { std::slice::from_raw_parts(input, len) }; + let mut sum: i32 = 0; + + for &byte in slice { + sum += byte as i32; + } + + sum +} + +/// XOR encryption (simple cipher for testing) +#[no_mangle] +pub extern "C" fn xor_encrypt(data: *mut u8, len: usize, key: u8) { + if data.is_null() || len == 0 { + return; + } + + let slice = unsafe { std::slice::from_raw_parts_mut(data, len) }; + + for byte in slice.iter_mut() { + *byte ^= key; + } +} + +/// Hash function (simple for testing) +#[no_mangle] +pub extern "C" fn simple_hash(data: *const u8, len: usize) -> u32 { + if data.is_null() || len == 0 { + return 0; + } + + let slice = unsafe { std::slice::from_raw_parts(data, len) }; + let mut hash: u32 = 5381; + + for &byte in slice { + hash = hash.wrapping_mul(33).wrapping_add(byte as u32); + } + + hash +} + +/// Initialize a TestStruct +#[no_mangle] +pub extern "C" fn init_struct(ts: *mut TestStruct, value: i32) { + if ts.is_null() { + return; + } + + unsafe { + (*ts).value = value; + (*ts).name = [0; 32]; + } +} + +/// Internal helper (not exported) +fn internal_helper(x: i32) -> i32 { + x * 2 + 1 +} + +/// Main function that uses the other functions +fn main() { + println!("{}", HELLO_WORLD); + println!("{}", VERSION_STRING); + + let sum = add_numbers(10, 20); + println!("10 + 20 = {}", sum); + + let product = multiply(5, 6); + println!("5 * 6 = {}", product); + + let fact = factorial(10); + println!("10! = {}", fact); + + let fib = fibonacci(20); + println!("fib(20) = {}", fib); + + let hash = simple_hash(SECRET_KEY.as_ptr(), SECRET_KEY.len()); + println!("hash = {:x}", hash); + + let helper_result = internal_helper(42); + println!("internal: {}", helper_result); + + // Create a simple lookup table + let mut lookup: HashMap<&str, i32> = HashMap::new(); + lookup.insert("one", 1); + lookup.insert("two", 2); + lookup.insert("three", 3); + + for (key, value) in &lookup { + println!("{} = {}", key, value); + } +}