From a550ad1d35c8295e739647a1e8ae0d0580adca71 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Wed, 4 Feb 2026 14:14:15 -0800 Subject: [PATCH] more work on v2 java bridge --- .github/workflows/release.yml | 12 +- .github/workflows/test.yml | 12 +- src/daemon/mod.rs | 22 +- src/ghidra/bridge.rs | 1013 +++++++------ src/ghidra/data.rs | 174 --- src/ghidra/mod.rs | 4 - src/ghidra/scripts.rs | 385 ----- src/ghidra/scripts/batch.py | 22 - src/ghidra/scripts/bridge.py | 1226 ---------------- src/ghidra/scripts/comments.py | 170 --- src/ghidra/scripts/diff.py | 132 -- src/ghidra/scripts/disasm.py | 86 -- src/ghidra/scripts/find.py | 259 ---- src/ghidra/scripts/graph.py | 223 --- src/ghidra/scripts/patch.py | 134 -- src/ghidra/scripts/program.py | 115 -- src/ghidra/scripts/script_runner.py | 121 -- src/ghidra/scripts/stats.py | 98 -- src/ghidra/scripts/symbols.py | 162 -- src/ghidra/scripts/types.py | 137 -- src/ghidra/setup.rs | 105 -- src/ipc/client.rs | 85 +- src/main.rs | 2123 +++++++++++++-------------- tests/batch_tests.rs | 15 +- tests/comment_tests.rs | 18 +- tests/common/mod.rs | 14 +- tests/daemon_tests.rs | 60 +- tests/diff_tests.rs | 6 +- tests/find_tests.rs | 24 +- tests/graph_tests.rs | 12 +- tests/program_tests.rs | 12 +- tests/reliability_tests.rs | 19 +- tests/script_tests.rs | 12 +- tests/stats_tests.rs | 9 +- tests/symbol_tests.rs | 18 +- tests/type_tests.rs | 15 +- 36 files changed, 1777 insertions(+), 5277 deletions(-) delete mode 100644 src/ghidra/data.rs delete mode 100644 src/ghidra/scripts.rs delete mode 100644 src/ghidra/scripts/batch.py delete mode 100644 src/ghidra/scripts/bridge.py delete mode 100644 src/ghidra/scripts/comments.py delete mode 100644 src/ghidra/scripts/diff.py delete mode 100644 src/ghidra/scripts/disasm.py delete mode 100644 src/ghidra/scripts/find.py delete mode 100644 src/ghidra/scripts/graph.py delete mode 100644 src/ghidra/scripts/patch.py delete mode 100644 src/ghidra/scripts/program.py delete mode 100644 src/ghidra/scripts/script_runner.py delete mode 100644 src/ghidra/scripts/stats.py delete mode 100644 src/ghidra/scripts/symbols.py delete mode 100644 src/ghidra/scripts/types.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51af5b9..94c0886 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,11 +26,6 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Install Python 3 - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -44,11 +39,14 @@ jobs: ~/.local/share/ghidra-cli ~/Library/Application Support/ghidra-cli ~/AppData/Local/ghidra-cli - key: ghidra-${{ matrix.os }}-v1 + key: ghidra-${{ matrix.os }}-v2 - name: Build run: cargo build --verbose + - name: Build test fixture + run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs + - name: Setup Ghidra run: cargo run -- setup --force @@ -57,6 +55,8 @@ jobs: - name: Run integration tests run: cargo test --test '*' --verbose + env: + RUST_LOG: info build: name: Build ${{ matrix.target }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90d3cbb..4020221 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,11 +26,6 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Install Python 3 - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -44,11 +39,14 @@ jobs: ~/.local/share/ghidra-cli ~/Library/Application Support/ghidra-cli ~/AppData/Local/ghidra-cli - key: ghidra-${{ matrix.os }}-v1 + key: ghidra-${{ matrix.os }}-v2 - name: Build run: cargo build --verbose + - name: Build test fixture + run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs + - name: Setup Ghidra run: cargo run -- setup --force @@ -57,3 +55,5 @@ jobs: - name: Run integration tests run: cargo test --test '*' --verbose + env: + RUST_LOG: info diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 380df05..935b1cd 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -22,28 +22,14 @@ pub struct BridgeConfig { /// Ensure a bridge is running for the given project. /// If import mode, starts with the binary. If process mode, opens existing program. /// Returns the port number for connecting. -pub fn ensure_bridge( - config: &BridgeConfig, - mode: BridgeStartMode, -) -> Result { - bridge::ensure_bridge_running( - &config.project_path, - &config.ghidra_install_dir, - mode, - ) +pub fn ensure_bridge(config: &BridgeConfig, mode: BridgeStartMode) -> Result { + bridge::ensure_bridge_running(&config.project_path, &config.ghidra_install_dir, mode) } /// Start a new bridge for the given project. /// Returns the port number for connecting. -pub fn start_bridge( - config: &BridgeConfig, - mode: BridgeStartMode, -) -> Result { - bridge::start_bridge( - &config.project_path, - &config.ghidra_install_dir, - mode, - ) +pub fn start_bridge(config: &BridgeConfig, mode: BridgeStartMode) -> Result { + bridge::start_bridge(&config.project_path, &config.ghidra_install_dir, mode) } /// Stop the bridge for a project. diff --git a/src/ghidra/bridge.rs b/src/ghidra/bridge.rs index 3211852..d225d2e 100644 --- a/src/ghidra/bridge.rs +++ b/src/ghidra/bridge.rs @@ -1,514 +1,499 @@ -//! Ghidra Bridge - manages a persistent Ghidra Java bridge process. -//! -//! The bridge runs a GhidraCliBridge.java script via `analyzeHeadless` that -//! starts a TCP socket server. The CLI connects directly to this server -//! to execute commands. No intermediate daemon process is needed. - -use std::io::{BufRead, BufReader, Write}; -use std::net::TcpStream; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; -use std::time::Duration; - -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use tracing::{debug, info, warn}; - -/// Response from the bridge -#[derive(Debug, Deserialize)] -pub struct BridgeResponse { - pub status: String, - pub data: Option, - #[serde(default)] - pub message: Option, -} - -/// Request to the bridge -#[derive(Debug, Serialize)] -struct BridgeRequest { - command: String, - #[serde(skip_serializing_if = "Option::is_none")] - args: Option, -} - -/// How to start the bridge - import a new binary or open an existing program. -pub enum BridgeStartMode { - /// Import a binary file into the project, then start bridge - Import { - binary_path: String, - }, - /// Open an existing program in the project - Process { - program_name: String, - }, -} - -/// Embedded Java bridge script -const JAVA_BRIDGE_SCRIPT: &str = include_str!("scripts/GhidraCliBridge.java"); - -/// Get the data directory for bridge port/PID files. -pub fn get_data_dir() -> Result { - let dir = dirs::data_local_dir() - .ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))? - .join("ghidra-cli"); - std::fs::create_dir_all(&dir)?; - Ok(dir) -} - -/// Compute MD5 hash of project path for file naming. -fn project_hash(project_path: &Path) -> String { - format!( - "{:x}", - md5::compute(project_path.to_string_lossy().as_bytes()) - ) -} - -/// Get the port file path for a project. -pub fn port_file_path(project_path: &Path) -> Result { - let data_dir = get_data_dir()?; - let hash = project_hash(project_path); - Ok(data_dir.join(format!("bridge-{}.port", hash))) -} - -/// Get the PID file path for a project. -pub fn pid_file_path(project_path: &Path) -> Result { - let data_dir = get_data_dir()?; - let hash = project_hash(project_path); - Ok(data_dir.join(format!("bridge-{}.pid", hash))) -} - -/// Read the port from the port file. -pub fn read_port_file(project_path: &Path) -> Result> { - let path = port_file_path(project_path)?; - if !path.exists() { - return Ok(None); - } - let content = std::fs::read_to_string(&path)?; - let port: u16 = content.trim().parse() - .context("Invalid port in port file")?; - Ok(Some(port)) -} - -/// Read the PID from the PID file. -pub fn read_pid_file(project_path: &Path) -> Result> { - let path = pid_file_path(project_path)?; - if !path.exists() { - return Ok(None); - } - let content = std::fs::read_to_string(&path)?; - let pid: u32 = content.trim().parse() - .context("Invalid PID in PID file")?; - Ok(Some(pid)) -} - -/// Check if a process with the given PID is alive. -pub fn is_pid_alive(pid: u32) -> bool { - #[cfg(unix)] - { - unsafe { libc::kill(pid as i32, 0) == 0 } - } - #[cfg(windows)] - { - use std::process::Command; - Command::new("tasklist") - .args(["/FI", &format!("PID eq {}", pid)]) - .output() - .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string())) - .unwrap_or(false) - } -} - -/// Clean up stale port and PID files. -pub fn cleanup_stale_files(project_path: &Path) -> Result<()> { - let port_path = port_file_path(project_path)?; - let pid_path = pid_file_path(project_path)?; - if port_path.exists() { - std::fs::remove_file(&port_path).ok(); - } - if pid_path.exists() { - std::fs::remove_file(&pid_path).ok(); - } - Ok(()) -} - -/// Check if a bridge is running for the given project. -/// -/// Verifies: port file exists, PID is alive, TCP connect succeeds. -pub fn is_bridge_running(project_path: &Path) -> bool { - let port = match read_port_file(project_path) { - Ok(Some(p)) => p, - _ => return false, - }; - - let pid = match read_pid_file(project_path) { - Ok(Some(p)) => p, - _ => return false, - }; - - if !is_pid_alive(pid) { - return false; - } - - // Verify TCP connect - TcpStream::connect(format!("127.0.0.1:{}", port)) - .map(|_| true) - .unwrap_or(false) -} - -/// Ensure a bridge is running for the given project. -/// Returns the port number to connect to. -pub fn ensure_bridge_running( - project_path: &Path, - ghidra_install_dir: &Path, - mode: BridgeStartMode, -) -> Result { - // Check if already running - if let Ok(Some(port)) = read_port_file(project_path) { - if let Ok(Some(pid)) = read_pid_file(project_path) { - if is_pid_alive(pid) { - // Verify TCP connect - if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { - info!("Bridge already running on port {}", port); - return Ok(port); - } - } - } - // Stale files - clean up - cleanup_stale_files(project_path)?; - } - - // Start a new bridge - start_bridge(project_path, ghidra_install_dir, mode) -} - -/// Start a new bridge process. -/// Returns the port number once the bridge is ready. -pub fn start_bridge( - project_path: &Path, - ghidra_install_dir: &Path, - mode: BridgeStartMode, -) -> Result { - info!("Starting Ghidra bridge..."); - - // Write the Java bridge script to disk - let scripts_dir = dirs::config_dir() - .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))? - .join("ghidra-cli") - .join("scripts"); - std::fs::create_dir_all(&scripts_dir)?; - let java_script_path = scripts_dir.join("GhidraCliBridge.java"); - std::fs::write(&java_script_path, JAVA_BRIDGE_SCRIPT)?; - - // Find analyzeHeadless - let headless_script = find_headless_script(ghidra_install_dir)?; - - // Compute port file path - let port_file = port_file_path(project_path)?; - - // Build command - let mut cmd = Command::new(&headless_script); - - // analyzeHeadless expects: - let ghidra_project_dir = project_path - .parent() - .unwrap_or(project_path); - let ghidra_project_name = project_path - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "project".to_string()); - - cmd.arg(ghidra_project_dir) - .arg(&ghidra_project_name); - - // Add mode-specific args - match &mode { - BridgeStartMode::Import { binary_path } => { - cmd.arg("-import").arg(binary_path); - } - BridgeStartMode::Process { program_name } => { - cmd.arg("-process") - .arg(program_name) - .arg("-noanalysis"); - } - } - - // Add Java bridge script args - cmd.arg("-scriptPath") - .arg(scripts_dir.to_str().unwrap()) - .arg("-postScript") - .arg("GhidraCliBridge.java") - .arg(port_file.to_str().unwrap()); - - cmd.stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - info!("Ghidra command: {:?}", cmd); - - // Spawn the process - let mut child = cmd.spawn().context("Failed to spawn Ghidra headless")?; - info!("Ghidra process started with PID: {:?}", child.id()); - - // Spawn a thread to capture stderr - let stderr = child.stderr.take().expect("stderr should be piped"); - let stderr_handle = std::thread::spawn(move || { - let reader = BufReader::new(stderr); - let mut stderr_output = Vec::new(); - for line in reader.lines() { - if let Ok(line) = line { - info!("[Ghidra stderr] {}", line); - stderr_output.push(line); - } - } - stderr_output - }); - - // Wait for ready signal from stdout - let stdout = child.stdout.take().expect("stdout should be piped"); - let reader = BufReader::new(stdout); - - let mut ready = false; - let mut last_error = String::new(); - let mut stdout_lines = Vec::new(); - for line in reader.lines() { - let line = line?; - info!("[Ghidra stdout] {}", line); - stdout_lines.push(line.clone()); - - if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") { - last_error = line.clone(); - } - - if line.contains("---GHIDRA_CLI_START---") { - continue; - } - if line.contains("\"status\"") && line.contains("\"ready\"") { - info!("Bridge is ready"); - ready = true; - break; - } - if line.contains("---GHIDRA_CLI_END---") && ready { - break; - } - } - - if !ready { - let stderr_output = stderr_handle.join().unwrap_or_default(); - let detail = if !last_error.is_empty() { - format!(": {}", last_error) - } else if !stderr_output.is_empty() { - let last_stderr: Vec<_> = stderr_output.iter().rev().take(5).rev().cloned().collect(); - format!(": stderr: {}", last_stderr.join("\n")) - } else { - let last_stdout: Vec<_> = stdout_lines.iter().rev().take(10).rev().cloned().collect(); - format!("\nLast stdout:\n{}", last_stdout.join("\n")) - }; - match child.try_wait() { - Ok(Some(status)) => { - anyhow::bail!("Ghidra process exited with status: {}{}", status, detail); - } - Ok(None) => { - anyhow::bail!("Ghidra bridge did not send ready signal{}", detail); - } - Err(e) => { - anyhow::bail!("Error checking process status: {}", e); - } - } - } - - // Read port from port file - let port = read_port_file(project_path)? - .ok_or_else(|| anyhow::anyhow!("Port file not created by bridge"))?; - - info!("Ghidra bridge started on port {}", port); - Ok(port) -} - -/// Send a command to the bridge and return the response. -pub fn send_command( - port: u16, - command: &str, - args: Option, -) -> Result { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)) - .context("Failed to connect to bridge")?; - stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); - stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); - - let request = BridgeRequest { - command: command.to_string(), - args, - }; - - let request_json = serde_json::to_string(&request)?; - debug!("Sending: {}", request_json); - - writeln!(stream, "{}", request_json)?; - stream.flush()?; - - let mut reader = BufReader::new(&stream); - let mut response_line = String::new(); - reader.read_line(&mut response_line)?; - - debug!("Received: {}", response_line.trim()); - - let response: BridgeResponse = serde_json::from_str(&response_line)?; - - match response.status.as_str() { - "success" => { - Ok(response.data.unwrap_or(serde_json::json!({}))) - } - "error" => { - let msg = response.message.unwrap_or_else(|| "Unknown error".to_string()); - anyhow::bail!("{}", msg) - } - "shutdown" => { - Ok(serde_json::json!({"status": "shutdown"})) - } - _ => { - Ok(response.data.unwrap_or(serde_json::json!({}))) - } - } -} - -/// Send a typed command to the bridge. -pub fn send_typed_command Deserialize<'de>>( - port: u16, - command: &str, - args: Option, -) -> Result> { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)) - .context("Failed to connect to bridge")?; - stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); - stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); - - let request = BridgeRequest { - command: command.to_string(), - args, - }; - - let request_json = serde_json::to_string(&request)?; - debug!("Sending: {}", request_json); - - writeln!(stream, "{}", request_json)?; - stream.flush()?; - - let mut reader = BufReader::new(&stream); - let mut response_line = String::new(); - reader.read_line(&mut response_line)?; - - debug!("Received: {}", response_line.trim()); - - let response: BridgeResponse = serde_json::from_str(&response_line)?; - Ok(response) -} - -/// Stop the bridge for a project. -pub fn stop_bridge(project_path: &Path) -> Result<()> { - // Try graceful shutdown via TCP - if let Ok(Some(port)) = read_port_file(project_path) { - if let Ok(response) = send_command(port, "shutdown", None) { - debug!("Shutdown response: {:?}", response); - } - } - - // If PID file exists, kill the process as fallback - if let Ok(Some(pid)) = read_pid_file(project_path) { - if is_pid_alive(pid) { - warn!("Killing bridge process {} as fallback", pid); - #[cfg(unix)] - unsafe { - libc::kill(pid as i32, libc::SIGTERM); - } - #[cfg(windows)] - { - let _ = std::process::Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/F"]) - .output(); - } - } - } - - // Clean up files - cleanup_stale_files(project_path)?; - - info!("Bridge stopped"); - Ok(()) -} - -/// Get bridge status for a project. -pub fn bridge_status(project_path: &Path) -> Result { - let port = read_port_file(project_path)?; - let pid = read_pid_file(project_path)?; - - if let (Some(port), Some(pid)) = (port, pid) { - if is_pid_alive(pid) { - if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { - return Ok(BridgeStatus::Running { port, pid }); - } - } - // Stale files - cleanup_stale_files(project_path).ok(); - } - - Ok(BridgeStatus::Stopped) -} - -/// Bridge status -#[derive(Debug)] -pub enum BridgeStatus { - Running { port: u16, pid: u32 }, - Stopped, -} - -/// Find the analyzeHeadless script. -fn find_headless_script(ghidra_install_dir: &Path) -> Result { - let support_dir = ghidra_install_dir.join("support"); - - #[cfg(unix)] - let script_name = "analyzeHeadless"; - #[cfg(windows)] - let script_name = "analyzeHeadless.bat"; - - let script_path = support_dir.join(script_name); - - if script_path.exists() { - Ok(script_path) - } else { - anyhow::bail!( - "analyzeHeadless not found at: {}", - support_dir.display() - ) - } -} - -/// Convenience wrapper for wait_timeout on Child -trait ChildExt { - fn wait_timeout( - &mut self, - timeout: Duration, - ) -> std::io::Result>; -} - -impl ChildExt for Child { - fn wait_timeout( - &mut self, - timeout: Duration, - ) -> std::io::Result> { - use std::thread; - use std::time::Instant; - - let start = Instant::now(); - loop { - match self.try_wait()? { - Some(status) => return Ok(Some(status)), - None => { - if start.elapsed() >= timeout { - return Ok(None); - } - thread::sleep(Duration::from_millis(100)); - } - } - } - } -} +//! Ghidra Bridge - manages a persistent Ghidra Java bridge process. +//! +//! The bridge runs a GhidraCliBridge.java script via `analyzeHeadless` that +//! starts a TCP socket server. The CLI connects directly to this server +//! to execute commands. No intermediate daemon process is needed. + +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use tracing::{debug, info, warn}; + +/// Response from the bridge +#[derive(Debug, Deserialize)] +pub struct BridgeResponse { + pub status: String, + pub data: Option, + #[serde(default)] + pub message: Option, +} + +/// Request to the bridge +#[derive(Debug, Serialize)] +struct BridgeRequest { + command: String, + #[serde(skip_serializing_if = "Option::is_none")] + args: Option, +} + +/// How to start the bridge - import a new binary or open an existing program. +pub enum BridgeStartMode { + /// Import a binary file into the project, then start bridge + Import { binary_path: String }, + /// Open an existing program in the project + Process { program_name: String }, +} + +/// Embedded Java bridge script +const JAVA_BRIDGE_SCRIPT: &str = include_str!("scripts/GhidraCliBridge.java"); + +/// Get the data directory for bridge port/PID files. +pub fn get_data_dir() -> Result { + let dir = dirs::data_local_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))? + .join("ghidra-cli"); + std::fs::create_dir_all(&dir)?; + Ok(dir) +} + +/// Compute MD5 hash of project path for file naming. +fn project_hash(project_path: &Path) -> String { + format!( + "{:x}", + md5::compute(project_path.to_string_lossy().as_bytes()) + ) +} + +/// Get the port file path for a project. +pub fn port_file_path(project_path: &Path) -> Result { + let data_dir = get_data_dir()?; + let hash = project_hash(project_path); + Ok(data_dir.join(format!("bridge-{}.port", hash))) +} + +/// Get the PID file path for a project. +pub fn pid_file_path(project_path: &Path) -> Result { + let data_dir = get_data_dir()?; + let hash = project_hash(project_path); + Ok(data_dir.join(format!("bridge-{}.pid", hash))) +} + +/// Read the port from the port file. +pub fn read_port_file(project_path: &Path) -> Result> { + let path = port_file_path(project_path)?; + if !path.exists() { + return Ok(None); + } + let content = std::fs::read_to_string(&path)?; + let port: u16 = content + .trim() + .parse() + .context("Invalid port in port file")?; + Ok(Some(port)) +} + +/// Read the PID from the PID file. +pub fn read_pid_file(project_path: &Path) -> Result> { + let path = pid_file_path(project_path)?; + if !path.exists() { + return Ok(None); + } + let content = std::fs::read_to_string(&path)?; + let pid: u32 = content.trim().parse().context("Invalid PID in PID file")?; + Ok(Some(pid)) +} + +/// Check if a process with the given PID is alive. +pub fn is_pid_alive(pid: u32) -> bool { + #[cfg(unix)] + { + unsafe { libc::kill(pid as i32, 0) == 0 } + } + #[cfg(windows)] + { + use std::process::Command; + Command::new("tasklist") + .args(["/FI", &format!("PID eq {}", pid)]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string())) + .unwrap_or(false) + } +} + +/// Clean up stale port and PID files. +pub fn cleanup_stale_files(project_path: &Path) -> Result<()> { + let port_path = port_file_path(project_path)?; + let pid_path = pid_file_path(project_path)?; + if port_path.exists() { + std::fs::remove_file(&port_path).ok(); + } + if pid_path.exists() { + std::fs::remove_file(&pid_path).ok(); + } + Ok(()) +} + +/// Check if a bridge is running for the given project. +/// +/// Verifies: port file exists, PID is alive, TCP connect succeeds. +pub fn is_bridge_running(project_path: &Path) -> bool { + let port = match read_port_file(project_path) { + Ok(Some(p)) => p, + _ => return false, + }; + + let pid = match read_pid_file(project_path) { + Ok(Some(p)) => p, + _ => return false, + }; + + if !is_pid_alive(pid) { + return false; + } + + // Verify TCP connect + TcpStream::connect(format!("127.0.0.1:{}", port)) + .map(|_| true) + .unwrap_or(false) +} + +/// Ensure a bridge is running for the given project. +/// Returns the port number to connect to. +pub fn ensure_bridge_running( + project_path: &Path, + ghidra_install_dir: &Path, + mode: BridgeStartMode, +) -> Result { + // Check if already running + if let Ok(Some(port)) = read_port_file(project_path) { + if let Ok(Some(pid)) = read_pid_file(project_path) { + if is_pid_alive(pid) { + // Verify TCP connect + if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { + info!("Bridge already running on port {}", port); + return Ok(port); + } + } + } + // Stale files - clean up + cleanup_stale_files(project_path)?; + } + + // Start a new bridge + start_bridge(project_path, ghidra_install_dir, mode) +} + +/// Start a new bridge process. +/// Returns the port number once the bridge is ready. +pub fn start_bridge( + project_path: &Path, + ghidra_install_dir: &Path, + mode: BridgeStartMode, +) -> Result { + info!("Starting Ghidra bridge..."); + + // Write the Java bridge script to disk + let scripts_dir = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))? + .join("ghidra-cli") + .join("scripts"); + std::fs::create_dir_all(&scripts_dir)?; + let java_script_path = scripts_dir.join("GhidraCliBridge.java"); + std::fs::write(&java_script_path, JAVA_BRIDGE_SCRIPT)?; + + // Find analyzeHeadless + let headless_script = find_headless_script(ghidra_install_dir)?; + + // Compute port file path + let port_file = port_file_path(project_path)?; + + // Build command + let mut cmd = Command::new(&headless_script); + + // analyzeHeadless expects: + let ghidra_project_dir = project_path.parent().unwrap_or(project_path); + let ghidra_project_name = project_path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "project".to_string()); + + cmd.arg(ghidra_project_dir).arg(&ghidra_project_name); + + // Add mode-specific args + match &mode { + BridgeStartMode::Import { binary_path } => { + cmd.arg("-import").arg(binary_path); + } + BridgeStartMode::Process { program_name } => { + cmd.arg("-process").arg(program_name).arg("-noanalysis"); + } + } + + // Add Java bridge script args + cmd.arg("-scriptPath") + .arg(scripts_dir.to_str().unwrap()) + .arg("-postScript") + .arg("GhidraCliBridge.java") + .arg(port_file.to_str().unwrap()); + + cmd.stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + info!("Ghidra command: {:?}", cmd); + + // Spawn the process + let mut child = cmd.spawn().context("Failed to spawn Ghidra headless")?; + info!("Ghidra process started with PID: {:?}", child.id()); + + // Spawn a thread to capture stderr + let stderr = child.stderr.take().expect("stderr should be piped"); + let stderr_handle = std::thread::spawn(move || { + let reader = BufReader::new(stderr); + let mut stderr_output = Vec::new(); + for line in reader.lines() { + if let Ok(line) = line { + info!("[Ghidra stderr] {}", line); + stderr_output.push(line); + } + } + stderr_output + }); + + // Wait for ready signal from stdout + let stdout = child.stdout.take().expect("stdout should be piped"); + let reader = BufReader::new(stdout); + + let mut ready = false; + let mut last_error = String::new(); + let mut stdout_lines = Vec::new(); + for line in reader.lines() { + let line = line?; + info!("[Ghidra stdout] {}", line); + stdout_lines.push(line.clone()); + + if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") { + last_error = line.clone(); + } + + if line.contains("---GHIDRA_CLI_START---") { + continue; + } + if line.contains("\"status\"") && line.contains("\"ready\"") { + info!("Bridge is ready"); + ready = true; + break; + } + if line.contains("---GHIDRA_CLI_END---") && ready { + break; + } + } + + if !ready { + let stderr_output = stderr_handle.join().unwrap_or_default(); + let detail = if !last_error.is_empty() { + format!(": {}", last_error) + } else if !stderr_output.is_empty() { + let last_stderr: Vec<_> = stderr_output.iter().rev().take(5).rev().cloned().collect(); + format!(": stderr: {}", last_stderr.join("\n")) + } else { + let last_stdout: Vec<_> = stdout_lines.iter().rev().take(10).rev().cloned().collect(); + format!("\nLast stdout:\n{}", last_stdout.join("\n")) + }; + match child.try_wait() { + Ok(Some(status)) => { + anyhow::bail!("Ghidra process exited with status: {}{}", status, detail); + } + Ok(None) => { + anyhow::bail!("Ghidra bridge did not send ready signal{}", detail); + } + Err(e) => { + anyhow::bail!("Error checking process status: {}", e); + } + } + } + + // Read port from port file + let port = read_port_file(project_path)? + .ok_or_else(|| anyhow::anyhow!("Port file not created by bridge"))?; + + info!("Ghidra bridge started on port {}", port); + Ok(port) +} + +/// Send a command to the bridge and return the response. +pub fn send_command( + port: u16, + command: &str, + args: Option, +) -> Result { + let mut stream = + TcpStream::connect(format!("127.0.0.1:{}", port)).context("Failed to connect to bridge")?; + stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); + stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); + + let request = BridgeRequest { + command: command.to_string(), + args, + }; + + let request_json = serde_json::to_string(&request)?; + debug!("Sending: {}", request_json); + + writeln!(stream, "{}", request_json)?; + stream.flush()?; + + let mut reader = BufReader::new(&stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; + + debug!("Received: {}", response_line.trim()); + + let response: BridgeResponse = serde_json::from_str(&response_line)?; + + match response.status.as_str() { + "success" => Ok(response.data.unwrap_or(serde_json::json!({}))), + "error" => { + let msg = response + .message + .unwrap_or_else(|| "Unknown error".to_string()); + anyhow::bail!("{}", msg) + } + "shutdown" => Ok(serde_json::json!({"status": "shutdown"})), + _ => Ok(response.data.unwrap_or(serde_json::json!({}))), + } +} + +/// Send a typed command to the bridge. +pub fn send_typed_command Deserialize<'de>>( + port: u16, + command: &str, + args: Option, +) -> Result> { + let mut stream = + TcpStream::connect(format!("127.0.0.1:{}", port)).context("Failed to connect to bridge")?; + stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); + stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); + + let request = BridgeRequest { + command: command.to_string(), + args, + }; + + let request_json = serde_json::to_string(&request)?; + debug!("Sending: {}", request_json); + + writeln!(stream, "{}", request_json)?; + stream.flush()?; + + let mut reader = BufReader::new(&stream); + let mut response_line = String::new(); + reader.read_line(&mut response_line)?; + + debug!("Received: {}", response_line.trim()); + + let response: BridgeResponse = serde_json::from_str(&response_line)?; + Ok(response) +} + +/// Stop the bridge for a project. +pub fn stop_bridge(project_path: &Path) -> Result<()> { + // Try graceful shutdown via TCP + if let Ok(Some(port)) = read_port_file(project_path) { + if let Ok(response) = send_command(port, "shutdown", None) { + debug!("Shutdown response: {:?}", response); + } + } + + // If PID file exists, kill the process as fallback + if let Ok(Some(pid)) = read_pid_file(project_path) { + if is_pid_alive(pid) { + warn!("Killing bridge process {} as fallback", pid); + #[cfg(unix)] + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } + #[cfg(windows)] + { + let _ = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output(); + } + } + } + + // Clean up files + cleanup_stale_files(project_path)?; + + info!("Bridge stopped"); + Ok(()) +} + +/// Get bridge status for a project. +pub fn bridge_status(project_path: &Path) -> Result { + let port = read_port_file(project_path)?; + let pid = read_pid_file(project_path)?; + + if let (Some(port), Some(pid)) = (port, pid) { + if is_pid_alive(pid) { + if TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { + return Ok(BridgeStatus::Running { port, pid }); + } + } + // Stale files + cleanup_stale_files(project_path).ok(); + } + + Ok(BridgeStatus::Stopped) +} + +/// Bridge status +#[derive(Debug)] +pub enum BridgeStatus { + Running { port: u16, pid: u32 }, + Stopped, +} + +/// Find the analyzeHeadless script. +fn find_headless_script(ghidra_install_dir: &Path) -> Result { + let support_dir = ghidra_install_dir.join("support"); + + #[cfg(unix)] + let script_name = "analyzeHeadless"; + #[cfg(windows)] + let script_name = "analyzeHeadless.bat"; + + let script_path = support_dir.join(script_name); + + if script_path.exists() { + Ok(script_path) + } else { + anyhow::bail!("analyzeHeadless not found at: {}", support_dir.display()) + } +} + +/// Convenience wrapper for wait_timeout on Child +trait ChildExt { + fn wait_timeout( + &mut self, + timeout: Duration, + ) -> std::io::Result>; +} + +impl ChildExt for Child { + fn wait_timeout( + &mut self, + timeout: Duration, + ) -> std::io::Result> { + use std::thread; + use std::time::Instant; + + let start = Instant::now(); + loop { + match self.try_wait()? { + Some(status) => return Ok(Some(status)), + None => { + if start.elapsed() >= timeout { + return Ok(None); + } + thread::sleep(Duration::from_millis(100)); + } + } + } + } +} diff --git a/src/ghidra/data.rs b/src/ghidra/data.rs deleted file mode 100644 index 972e6ef..0000000 --- a/src/ghidra/data.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Data structures for Ghidra query results. -//! -//! These are used to parse JSON responses from Ghidra scripts. - -#![allow(dead_code)] - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Function { - pub name: String, - pub address: String, - pub size: u64, - pub signature: Option, - pub entry_point: String, - pub calling_convention: Option, - #[serde(default)] - pub parameters: Vec, - #[serde(default)] - pub local_variables: Vec, - #[serde(default)] - pub calls: Vec, - #[serde(default)] - pub called_by: Vec, - pub decompiled: Option, - pub comment: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Parameter { - pub name: String, - pub data_type: String, - pub ordinal: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LocalVariable { - pub name: String, - pub data_type: String, - pub stack_offset: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StringData { - pub address: String, - pub value: String, - pub length: usize, - pub encoding: String, - #[serde(default)] - pub references: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Symbol { - pub name: String, - pub address: String, - pub symbol_type: String, - pub namespace: Option, - pub source: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Import { - pub name: String, - pub address: String, - pub library: String, - pub ordinal: Option, - pub is_external: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Export { - pub name: String, - pub address: String, - pub ordinal: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct XRef { - pub from: String, - pub to: String, - pub ref_type: String, - pub from_function: Option, - pub to_function: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryBlock { - pub name: String, - pub start: String, - pub end: String, - pub size: u64, - pub permissions: String, - pub is_initialized: bool, - pub is_loaded: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Section { - pub name: String, - pub address: String, - pub size: u64, - pub virtual_address: String, - pub file_offset: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Comment { - pub address: String, - pub comment_type: String, - pub text: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataType { - pub name: String, - pub category: String, - pub size: Option, - pub description: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Instruction { - pub address: String, - pub mnemonic: String, - pub operands: String, - pub bytes: String, - pub length: u32, - pub flow_type: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BasicBlock { - pub start: String, - pub end: String, - pub size: u64, - pub instruction_count: u32, - #[serde(default)] - pub successors: Vec, - #[serde(default)] - pub predecessors: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProgramInfo { - pub name: String, - pub executable_path: String, - pub executable_format: String, - pub compiler: Option, - pub language: String, - pub creation_date: Option, - pub image_base: String, - pub min_address: String, - pub max_address: String, - pub function_count: usize, - pub instruction_count: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum GhidraData { - Function(Function), - String(StringData), - Symbol(Symbol), - Import(Import), - Export(Export), - XRef(XRef), - MemoryBlock(MemoryBlock), - Section(Section), - Comment(Comment), - DataType(DataType), - Instruction(Instruction), - BasicBlock(BasicBlock), -} diff --git a/src/ghidra/mod.rs b/src/ghidra/mod.rs index 203167a..dfe9594 100644 --- a/src/ghidra/mod.rs +++ b/src/ghidra/mod.rs @@ -1,8 +1,6 @@ #![allow(dead_code)] pub mod bridge; -pub mod data; -pub mod scripts; pub mod setup; use crate::config::Config; @@ -42,13 +40,11 @@ impl GhidraClient { #[cfg(target_os = "windows")] { - // Use analyzeHeadless with Jython support support_dir.join("analyzeHeadless.bat") } #[cfg(not(target_os = "windows"))] { - // Use analyzeHeadless with Jython support support_dir.join("analyzeHeadless") } } diff --git a/src/ghidra/scripts.rs b/src/ghidra/scripts.rs deleted file mode 100644 index 744b7f3..0000000 --- a/src/ghidra/scripts.rs +++ /dev/null @@ -1,385 +0,0 @@ -//! Built-in Ghidra scripts for data extraction -//! These are Python scripts that will be written to disk and executed by Ghidra headless - -pub fn get_list_functions_script() -> &'static str { - r#" -# List all functions in the program -# @category Analysis -# @runtime Jython - -import json - -functions = [] -function_manager = currentProgram.getFunctionManager() - -for func in function_manager.getFunctions(True): - entry = func.getEntryPoint() - body = func.getBody() - - func_data = { - "name": func.getName(), - "address": entry.toString(), - "size": body.getNumAddresses(), - "entry_point": entry.toString(), - "signature": func.getPrototypeString(False, False) if func.getSignature() else None, - "calling_convention": func.getCallingConventionName(), - "comment": func.getComment() - } - - # Get called functions - called = [] - refs = func.getBody().getAddresses(True) - for addr in refs: - for ref in currentProgram.getReferenceManager().getReferencesFrom(addr): - if ref.getReferenceType().isCall(): - to_addr = ref.getToAddress() - to_func = function_manager.getFunctionAt(to_addr) - if to_func: - called.append(to_func.getName()) - - func_data["calls"] = list(set(called)) - - # Get callers - callers = [] - refs_to = currentProgram.getReferenceManager().getReferencesTo(entry) - for ref in refs_to: - if ref.getReferenceType().isCall(): - from_addr = ref.getFromAddress() - from_func = function_manager.getFunctionContaining(from_addr) - if from_func: - callers.append(from_func.getName()) - - func_data["called_by"] = list(set(callers)) - - functions.append(func_data) - -print("---GHIDRA_CLI_START---") -print(json.dumps(functions, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_decompile_function_script() -> &'static str { - r#" -# Decompile a specific function by address or name -# @category Analysis -# @runtime Jython - -import json -from ghidra.app.decompiler import DecompInterface -from ghidra.util.task import ConsoleTaskMonitor - -# Get function target from args (can be address or name) -script_args = getScriptArgs() -if len(script_args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No target provided. Specify an address (0x...) or function name."})) - print("---GHIDRA_CLI_END---") - exit(1) - -target = script_args[0] -function_manager = currentProgram.getFunctionManager() -func = None - -# Try to parse as address first -if target.startswith("0x") or target.startswith("0X"): - # It's an address - addr = currentProgram.getAddressFactory().getAddress(target) - if addr: - func = function_manager.getFunctionContaining(addr) -elif target.isdigit() or (len(target) > 1 and target[0].isdigit()): - # Might be a hex address without 0x prefix - try: - addr = currentProgram.getAddressFactory().getAddress(target) - if addr: - func = function_manager.getFunctionContaining(addr) - except: - pass - -# If not found by address, try by name -if not func: - # Search for function by name (exact match first) - for f in function_manager.getFunctions(True): - if f.getName() == target: - func = f - break - - # If still not found, try partial match - if not func: - for f in function_manager.getFunctions(True): - if target in f.getName(): - func = f - break - -if not func: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No function found for: " + target})) - print("---GHIDRA_CLI_END---") - exit(1) - -# Decompile -decompiler = DecompInterface() -decompiler.openProgram(currentProgram) - -monitor = ConsoleTaskMonitor() -results = decompiler.decompileFunction(func, 30, monitor) - -if results.decompileCompleted(): - code = results.getDecompiledFunction().getC() - - result = { - "name": func.getName(), - "address": func.getEntryPoint().toString(), - "signature": func.getPrototypeString(False, False), - "code": code - } - - print("---GHIDRA_CLI_START---") - print(json.dumps(result, indent=2)) - print("---GHIDRA_CLI_END---") -else: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "Decompilation failed for: " + func.getName()})) - print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_list_strings_script() -> &'static str { - r#" -# List all strings in the program -# @category Analysis -# @runtime Jython - -import json - -strings = [] -listing = currentProgram.getListing() -data_iterator = listing.getDefinedData(True) - -while data_iterator.hasNext(): - data = data_iterator.next() - if data.hasStringValue(): - try: - # Get string value, handle Unicode properly - string_val = unicode(data.getValue()) - string_data = { - "address": str(data.getAddress()), - "value": string_val, - "length": len(string_val), - "encoding": "unicode" - } - - # Get references to this string - refs = [] - refs_to = currentProgram.getReferenceManager().getReferencesTo(data.getAddress()) - for ref in refs_to: - refs.append(str(ref.getFromAddress())) - - string_data["references"] = refs - strings.append(string_data) - except Exception as e: - # Skip strings that cause encoding issues - pass - -print("---GHIDRA_CLI_START---") -print(json.dumps(strings, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_list_imports_script() -> &'static str { - r#" -# List all imports in the program -# @category Analysis -# @runtime Jython - -import json - -imports = [] -symbol_table = currentProgram.getSymbolTable() -external_manager = currentProgram.getExternalManager() - -for symbol in symbol_table.getExternalSymbols(): - external_location = external_manager.getExternalLocation(symbol) - - if external_location: - import_data = { - "name": symbol.getName(), - "address": symbol.getAddress().toString(), - "library": external_location.getLibraryName(), - "is_external": True - } - imports.append(import_data) - -print("---GHIDRA_CLI_START---") -print(json.dumps(imports, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_list_exports_script() -> &'static str { - r#" -# List all exports in the program -# @category Analysis -# @runtime Jython - -import json - -exports = [] -symbol_table = currentProgram.getSymbolTable() - -for symbol in symbol_table.getSymbolIterator(): - if symbol.isExternalEntryPoint(): - export_data = { - "name": symbol.getName(), - "address": symbol.getAddress().toString() - } - exports.append(export_data) - -print("---GHIDRA_CLI_START---") -print(json.dumps(exports, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_memory_map_script() -> &'static str { - r#" -# Get memory map -# @category Analysis -# @runtime Jython - -import json - -blocks = [] -memory = currentProgram.getMemory() - -for block in memory.getBlocks(): - block_data = { - "name": block.getName(), - "start": block.getStart().toString(), - "end": block.getEnd().toString(), - "size": block.getSize(), - "permissions": "", - "is_initialized": block.isInitialized(), - "is_loaded": block.isLoaded() - } - - # Build permissions string - perms = "" - if block.isRead(): - perms += "r" - if block.isWrite(): - perms += "w" - if block.isExecute(): - perms += "x" - - block_data["permissions"] = perms - blocks.append(block_data) - -print("---GHIDRA_CLI_START---") -print(json.dumps(blocks, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_program_info_script() -> &'static str { - r#" -# Get program information -# @category Analysis -# @runtime Jython - -import json - -info = { - "name": currentProgram.getName(), - "executable_path": currentProgram.getExecutablePath(), - "executable_format": currentProgram.getExecutableFormat(), - "compiler": currentProgram.getCompiler() if currentProgram.getCompiler() else None, - "language": currentProgram.getLanguage().toString(), - "image_base": currentProgram.getImageBase().toString(), - "min_address": currentProgram.getMinAddress().toString(), - "max_address": currentProgram.getMaxAddress().toString() -} - -# Count functions and instructions -function_manager = currentProgram.getFunctionManager() -info["function_count"] = function_manager.getFunctionCount() - -instruction_count = 0 -listing = currentProgram.getListing() -for instruction in listing.getInstructions(True): - instruction_count += 1 - -info["instruction_count"] = instruction_count - -print("---GHIDRA_CLI_START---") -print(json.dumps(info, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -pub fn get_xrefs_to_script() -> &'static str { - r#" -# Get cross-references to an address -# @category Analysis -# @runtime Jython - -import json - -script_args = getScriptArgs() -if len(script_args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No address provided"})) - print("---GHIDRA_CLI_END---") - exit(1) - -addr_str = script_args[0] -addr = currentProgram.getAddressFactory().getAddress(addr_str) - -xrefs = [] -refs = currentProgram.getReferenceManager().getReferencesTo(addr) -function_manager = currentProgram.getFunctionManager() - -for ref in refs: - from_addr = ref.getFromAddress() - from_func = function_manager.getFunctionContaining(from_addr) - to_func = function_manager.getFunctionContaining(addr) - - xref_data = { - "from": from_addr.toString(), - "to": addr.toString(), - "ref_type": ref.getReferenceType().toString(), - "from_function": from_func.getName() if from_func else None, - "to_function": to_func.getName() if to_func else None - } - xrefs.append(xref_data) - -print("---GHIDRA_CLI_START---") -print(json.dumps(xrefs, indent=2)) -print("---GHIDRA_CLI_END---") -"# -} - -/// Save a script to disk -pub fn save_script( - name: &str, - content: &str, - scripts_dir: &std::path::Path, -) -> crate::error::Result { - // All scripts are Python now with PyGhidra support - let script_path = scripts_dir.join(format!("{}.py", name)); - std::fs::write(&script_path, content)?; - Ok(script_path) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_scripts_not_empty() { - assert!(!get_list_functions_script().is_empty()); - assert!(!get_decompile_function_script().is_empty()); - assert!(!get_list_strings_script().is_empty()); - } -} diff --git a/src/ghidra/scripts/batch.py b/src/ghidra/scripts/batch.py deleted file mode 100644 index 9b2b0ee..0000000 --- a/src/ghidra/scripts/batch.py +++ /dev/null @@ -1,22 +0,0 @@ -# Batch operations script -# @category CLI -# -# Note: Batch operations are handled directly in Rust handler. -# This script exists for consistency but is not actively used. - -import sys -import json - -def batch_placeholder(): - """Placeholder function - batch operations handled in Rust.""" - return {"error": "Batch operations are handled by the Rust daemon, not via Python script"} - -if __name__ == "__main__": - try: - print("---GHIDRA_CLI_START---") - print(json.dumps(batch_placeholder())) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/bridge.py b/src/ghidra/scripts/bridge.py deleted file mode 100644 index 4ef429e..0000000 --- a/src/ghidra/scripts/bridge.py +++ /dev/null @@ -1,1226 +0,0 @@ -# Ghidra CLI Bridge Script -# @category Bridge -# @keybinding -# @menupath Tools.Start CLI Bridge -# @toolbar -# -# This script runs a persistent TCP server inside Ghidra to serve CLI commands. -# It keeps Ghidra loaded in memory for fast command execution. - -import socket -import json -import threading -import sys -import os -from ghidra.util.task import ConsoleTaskMonitor -from ghidra.app.decompiler import DecompInterface - -# Default bridge port -BRIDGE_PORT = 18700 - -# Global registry for Ghidra objects that imported modules can access -import builtins -builtins.currentProgram = currentProgram -try: - builtins.currentAddress = currentAddress -except: - builtins.currentAddress = None -try: - builtins.currentLocation = currentLocation -except: - builtins.currentLocation = None -try: - builtins.state = state -except: - builtins.state = None -try: - builtins.monitor = monitor -except: - builtins.monitor = None - -# Helper to import modules with Ghidra globals injected -def import_ghidra_module(module_name): - """Import a module - Ghidra globals are available via builtins.""" - script_dir = os.path.dirname(os.path.realpath(__file__)) - if script_dir not in sys.path: - sys.path.insert(0, script_dir) - - # Force reimport to get fresh module - if module_name in sys.modules: - del sys.modules[module_name] - - module = __import__(module_name) - return module - -# --- Helpers --- - -def resolve_address(addr_str): - """Resolve an address string or function name to an Address object. - - Returns (address, error_string). On success error_string is None. - Tries parsing as address first, then looks up by function name. - """ - if currentProgram is None: - return None, "No program loaded" - - # Try as address first - addr = currentProgram.getAddressFactory().getAddress(addr_str) - if addr is not None: - return addr, None - - # Try as function name - function_manager = currentProgram.getFunctionManager() - for func in function_manager.getFunctions(True): - if func.getName() == addr_str: - return func.getEntryPoint(), None - - return None, "Cannot resolve address or function name: " + addr_str - -# --- Command Handlers --- - -def handle_ping(args): - """Health check.""" - return {"message": "pong"} - -def handle_program_info(args): - """Get current program information.""" - if currentProgram is None: - return {"error": "No program loaded"} - - info = { - "name": currentProgram.getName(), - "executable_path": currentProgram.getExecutablePath(), - "executable_format": currentProgram.getExecutableFormat(), - "compiler": currentProgram.getCompiler() if currentProgram.getCompiler() else None, - "language": str(currentProgram.getLanguage()), - "image_base": str(currentProgram.getImageBase()), - "min_address": str(currentProgram.getMinAddress()), - "max_address": str(currentProgram.getMaxAddress()) - } - - function_manager = currentProgram.getFunctionManager() - info["function_count"] = function_manager.getFunctionCount() - - return info - -def handle_list_functions(args): - """List all functions in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - limit = args.get("limit") - name_filter = args.get("filter") - - functions = [] - function_manager = currentProgram.getFunctionManager() - count = 0 - - for func in function_manager.getFunctions(True): - if limit and count >= limit: - break - - name = func.getName() - if name_filter and name_filter.lower() not in name.lower(): - continue - - entry = func.getEntryPoint() - body = func.getBody() - - func_data = { - "name": name, - "address": str(entry), - "size": body.getNumAddresses(), - "entry_point": str(entry), - "signature": func.getPrototypeString(False, False) if func.getSignature() else None, - "calling_convention": func.getCallingConventionName(), - "comment": func.getComment() - } - - functions.append(func_data) - count += 1 - - return {"functions": functions, "count": len(functions)} - -def handle_decompile(args): - """Decompile a function at the given address or by name.""" - if currentProgram is None: - return {"error": "No program loaded"} - - addr_str = args.get("address") - if not addr_str: - return {"error": "No address provided"} - - addr, err = resolve_address(addr_str) - if err: - return {"error": err} - - function_manager = currentProgram.getFunctionManager() - func = function_manager.getFunctionContaining(addr) - - if not func: - return {"error": "No function at address " + addr_str} - - decompiler = DecompInterface() - decompiler.openProgram(currentProgram) - - monitor = ConsoleTaskMonitor() - results = decompiler.decompileFunction(func, 30, monitor) - - if results.decompileCompleted(): - code = results.getDecompiledFunction().getC() - return { - "name": func.getName(), - "address": str(func.getEntryPoint()), - "signature": func.getPrototypeString(False, False), - "code": code - } - else: - return {"error": "Decompilation failed"} - -def handle_list_strings(args): - """List all strings in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - limit = args.get("limit") - - strings = [] - listing = currentProgram.getListing() - data_iterator = listing.getDefinedData(True) - count = 0 - - while data_iterator.hasNext(): - if limit and count >= limit: - break - - data = data_iterator.next() - if data.hasStringValue(): - try: - string_val = str(data.getValue()) - string_data = { - "address": str(data.getAddress()), - "value": string_val, - "length": len(string_val) - } - strings.append(string_data) - count += 1 - except Exception: - pass - - return {"strings": strings, "count": len(strings)} - -def handle_list_imports(args): - """List all imports in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - imports = [] - symbol_table = currentProgram.getSymbolTable() - external_manager = currentProgram.getExternalManager() - - for symbol in symbol_table.getExternalSymbols(): - external_location = external_manager.getExternalLocation(symbol) - - if external_location: - import_data = { - "name": symbol.getName(), - "address": str(symbol.getAddress()), - "library": external_location.getLibraryName() - } - imports.append(import_data) - - return {"imports": imports, "count": len(imports)} - -def handle_list_exports(args): - """List all exports in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - exports = [] - symbol_table = currentProgram.getSymbolTable() - - for symbol in symbol_table.getSymbolIterator(): - if symbol.isExternalEntryPoint(): - export_data = { - "name": symbol.getName(), - "address": str(symbol.getAddress()) - } - exports.append(export_data) - - return {"exports": exports, "count": len(exports)} - -def handle_memory_map(args): - """Get memory map.""" - if currentProgram is None: - return {"error": "No program loaded"} - - blocks = [] - memory = currentProgram.getMemory() - - for block in memory.getBlocks(): - perms = "" - if block.isRead(): - perms += "r" - if block.isWrite(): - perms += "w" - if block.isExecute(): - perms += "x" - - block_data = { - "name": block.getName(), - "start": str(block.getStart()), - "end": str(block.getEnd()), - "size": block.getSize(), - "permissions": perms, - "is_initialized": block.isInitialized(), - "is_loaded": block.isLoaded() - } - blocks.append(block_data) - - return {"blocks": blocks, "count": len(blocks)} - -def handle_xrefs_to(args): - """Get cross-references to an address or function.""" - if currentProgram is None: - return {"error": "No program loaded"} - - addr_str = args.get("address") - if not addr_str: - return {"error": "No address provided"} - - addr, err = resolve_address(addr_str) - if err: - return {"error": err} - - xrefs = [] - refs = currentProgram.getReferenceManager().getReferencesTo(addr) - function_manager = currentProgram.getFunctionManager() - - for ref in refs: - from_addr = ref.getFromAddress() - from_func = function_manager.getFunctionContaining(from_addr) - to_func = function_manager.getFunctionContaining(addr) - - xref_data = { - "from": str(from_addr), - "to": str(addr), - "ref_type": str(ref.getReferenceType()), - "from_function": from_func.getName() if from_func else None, - "to_function": to_func.getName() if to_func else None - } - xrefs.append(xref_data) - - return {"xrefs": xrefs, "count": len(xrefs)} - -def handle_xrefs_from(args): - """Get cross-references from an address or function.""" - if currentProgram is None: - return {"error": "No program loaded"} - - addr_str = args.get("address") - if not addr_str: - return {"error": "No address provided"} - - addr, err = resolve_address(addr_str) - if err: - return {"error": err} - - xrefs = [] - refs = currentProgram.getReferenceManager().getReferencesFrom(addr) - function_manager = currentProgram.getFunctionManager() - - for ref in refs: - to_addr = ref.getToAddress() - from_func = function_manager.getFunctionContaining(addr) - to_func = function_manager.getFunctionContaining(to_addr) - - xref_data = { - "from": str(addr), - "to": str(to_addr), - "ref_type": str(ref.getReferenceType()), - "from_function": from_func.getName() if from_func else None, - "to_function": to_func.getName() if to_func else None - } - xrefs.append(xref_data) - - return {"xrefs": xrefs, "count": len(xrefs)} - -def handle_program_close(args): - """Close the current program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - program_name = currentProgram.getName() - state.getTool().closeProgram(currentProgram, False) - - return {"status": "closed", "program": program_name} - -def handle_program_delete(args): - """Delete a program from the project.""" - program_name = args.get("program") - if not program_name: - return {"error": "Program name required"} - - project = state.getProject() - if project is None: - return {"error": "No project open"} - - project_data = project.getProjectData() - - try: - # Ghidra paths must start with / - path = program_name if program_name.startswith("/") else "/" + program_name - program_file = project_data.getFile(path) - if program_file is None: - return {"error": "Program not found: " + program_name} - - program_file.delete() - return {"status": "deleted", "program": program_name} - except Exception as e: - return {"error": "Failed to delete program: " + str(e)} - -def handle_program_export(args): - """Export program to specified format.""" - if currentProgram is None: - return {"error": "No program loaded"} - - export_format = args.get("format", "json") - output_path = args.get("output") - - if export_format == "json": - data = handle_program_info({}) - if "error" in data: - return data - - function_manager = currentProgram.getFunctionManager() - functions = [] - for func in function_manager.getFunctions(True): - functions.append({ - "name": func.getName(), - "address": str(func.getEntryPoint()), - "size": func.getBody().getNumAddresses() - }) - data["functions"] = functions - - if output_path: - try: - with open(output_path, 'w') as f: - json.dump(data, f, indent=2) - return {"status": "exported", "format": "json", "output": output_path} - except Exception as e: - return {"error": "Failed to write file: " + str(e)} - else: - return data - else: - return {"error": "Unsupported export format: " + export_format} - -# --- Command Router --- - -def handle_find_string(args): - """Find string references.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_strings(args.get("pattern", "")) - except Exception as e: - return {"error": "Failed to find strings: " + str(e)} - -def handle_find_bytes(args): - """Find byte patterns.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_bytes(args.get("hex", "")) - except Exception as e: - return {"error": "Failed to find bytes: " + str(e)} - -def handle_find_function(args): - """Find functions by pattern.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_functions(args.get("pattern", "")) - except Exception as e: - return {"error": "Failed to find functions: " + str(e)} - -def handle_find_calls(args): - """Find calls to function.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_calls(args.get("function", "")) - except Exception as e: - return {"error": "Failed to find calls: " + str(e)} - -def handle_find_crypto(args): - """Find crypto constants.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_crypto() - except Exception as e: - return {"error": "Failed to find crypto: " + str(e)} - -def handle_find_interesting(args): - """Find interesting functions.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import find - return find.find_interesting() - except Exception as e: - return {"error": "Failed to find interesting: " + str(e)} - -def handle_script_run(args): - """Run a script file.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import script_runner - return script_runner.run_script(args.get("path", ""), args.get("args", [])) - except Exception as e: - return {"error": "Failed to run script: " + str(e)} - -def handle_script_python(args): - """Execute Python code.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import script_runner - return script_runner.exec_python(args.get("code", "")) - except Exception as e: - return {"error": "Failed to execute Python: " + str(e)} - -def handle_script_java(args): - """Execute Java code.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import script_runner - return script_runner.exec_java(args.get("code", "")) - except Exception as e: - return {"error": "Failed to execute Java: " + str(e)} - -def handle_script_list(args): - """List available scripts.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import script_runner - return script_runner.list_scripts() - except Exception as e: - return {"error": "Failed to list scripts: " + str(e)} - -# --- Symbol Handlers --- - -def handle_symbol_list(args): - """List symbols.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import symbols - return symbols.list_symbols(args.get("filter")) - except Exception as e: - return {"error": "Failed to list symbols: " + str(e)} - -def handle_symbol_get(args): - """Get symbol details.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import symbols - return symbols.get_symbol(args.get("name", "")) - except Exception as e: - return {"error": "Failed to get symbol: " + str(e)} - -def handle_symbol_create(args): - """Create a symbol.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import symbols - return symbols.create_symbol(args.get("address", ""), args.get("name", "")) - except Exception as e: - return {"error": "Failed to create symbol: " + str(e)} - -def handle_symbol_delete(args): - """Delete a symbol.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import symbols - return symbols.delete_symbol(args.get("name", "")) - except Exception as e: - return {"error": "Failed to delete symbol: " + str(e)} - -def handle_symbol_rename(args): - """Rename a symbol.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import symbols - return symbols.rename_symbol(args.get("old_name", ""), args.get("new_name", "")) - except Exception as e: - return {"error": "Failed to rename symbol: " + str(e)} - -# --- Type Handlers --- - -def handle_type_list(args): - """List data types.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import importlib.util - spec = importlib.util.spec_from_file_location("ghidra_types", os.path.join(script_dir, "types.py")) - ghidra_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(ghidra_types) - return ghidra_types.list_types() - except Exception as e: - return {"error": "Failed to list types: " + str(e)} - -def handle_type_get(args): - """Get type details.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import importlib.util - spec = importlib.util.spec_from_file_location("ghidra_types", os.path.join(script_dir, "types.py")) - ghidra_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(ghidra_types) - return ghidra_types.get_type(args.get("name", "")) - except Exception as e: - return {"error": "Failed to get type: " + str(e)} - -def handle_type_create(args): - """Create a data type.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import importlib.util - spec = importlib.util.spec_from_file_location("ghidra_types", os.path.join(script_dir, "types.py")) - ghidra_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(ghidra_types) - # The Python script expects a type name; CLI passes "definition" as the name - return ghidra_types.create_type(args.get("definition", args.get("name", ""))) - except Exception as e: - return {"error": "Failed to create type: " + str(e)} - -def handle_type_apply(args): - """Apply a type to an address.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import importlib.util - spec = importlib.util.spec_from_file_location("ghidra_types", os.path.join(script_dir, "types.py")) - ghidra_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(ghidra_types) - return ghidra_types.apply_type(args.get("address", ""), args.get("type_name", "")) - except Exception as e: - return {"error": "Failed to apply type: " + str(e)} - -# --- Comment Handlers --- - -def handle_comment_list(args): - """List comments.""" - try: - comments = import_ghidra_module("comments") - return comments.list_comments() - except Exception as e: - return {"error": "Failed to list comments: " + str(e)} - -def handle_comment_get(args): - """Get comments at address.""" - try: - comments = import_ghidra_module("comments") - return comments.get_comments(args.get("address", "")) - except Exception as e: - return {"error": "Failed to get comments: " + str(e)} - -def handle_comment_set(args): - """Set a comment at address.""" - try: - comments = import_ghidra_module("comments") - comment_type = args.get("comment_type", "EOL") or "EOL" # Default to EOL - return comments.set_comment(args.get("address", ""), args.get("text", ""), comment_type) - except Exception as e: - return {"error": "Failed to set comment: " + str(e)} - -def handle_comment_delete(args): - """Delete comment at address.""" - try: - comments = import_ghidra_module("comments") - return comments.delete_comment(args.get("address", "")) - except Exception as e: - return {"error": "Failed to delete comment: " + str(e)} - -# --- Graph Handlers --- - -def handle_graph_calls(args): - """Get call graph.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import graph - return graph.get_call_graph(args.get("limit")) - except Exception as e: - return {"error": "Failed to get call graph: " + str(e)} - -def handle_graph_callers(args): - """Get callers of a function.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import graph - return graph.get_callers(args.get("function", ""), args.get("depth", 1)) - except Exception as e: - return {"error": "Failed to get callers: " + str(e)} - -def handle_graph_callees(args): - """Get callees of a function.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import graph - return graph.get_callees(args.get("function", ""), args.get("depth", 1)) - except Exception as e: - return {"error": "Failed to get callees: " + str(e)} - -def handle_graph_export(args): - """Export call graph.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import graph - return graph.export_graph(args.get("format", "dot")) - except Exception as e: - return {"error": "Failed to export graph: " + str(e)} - -# --- Diff Handlers --- - -def handle_diff_programs(args): - """Diff two programs.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import diff - return diff.diff_programs(args.get("program1", ""), args.get("program2", "")) - except Exception as e: - return {"error": "Failed to diff programs: " + str(e)} - -def handle_diff_functions(args): - """Diff two functions.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import diff - return diff.diff_functions(args.get("func1", ""), args.get("func2", "")) - except Exception as e: - return {"error": "Failed to diff functions: " + str(e)} - -# --- Patch Handlers --- - -def handle_patch_bytes(args): - """Patch bytes at address.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import patch - return patch.patch_bytes(args.get("address", ""), args.get("hex", "")) - except Exception as e: - return {"error": "Failed to patch bytes: " + str(e)} - -def handle_patch_nop(args): - """NOP instruction at address.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import patch - return patch.patch_nop(args.get("address", "")) - except Exception as e: - return {"error": "Failed to NOP: " + str(e)} - -def handle_patch_export(args): - """Export patches.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import patch - return patch.export_patches(args.get("output", "")) - except Exception as e: - return {"error": "Failed to export patches: " + str(e)} - -# --- Disasm Handler --- - -def handle_disasm(args): - """Disassemble at address or function name.""" - addr_str = args.get("address", "") - if addr_str: - addr, err = resolve_address(addr_str) - if err: - return {"error": err} - addr_str = str(addr) - - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import disasm - return disasm.disassemble(addr_str, args.get("count", 10)) - except Exception as e: - return {"error": "Failed to disassemble: " + str(e)} - -# --- Stats Handler --- - -def handle_stats(args): - """Get program statistics.""" - import sys - import os - script_dir = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, script_dir) - try: - import stats - return stats.get_stats() - except Exception as e: - return {"error": "Failed to get stats: " + str(e)} - -# --- Import/Analyze Handlers --- - -def handle_import(args): - """Import a binary into the current project.""" - from ghidra.app.util.importer import AutoImporter, MessageLog - from ghidra.util.task import ConsoleTaskMonitor - from java.io import File - - binary_path = args.get("binary_path") - if not binary_path: - return {"error": "No binary_path provided"} - - program_name = args.get("program") - if not program_name: - binary_file = File(binary_path) - program_name = binary_file.getName() - - project = state.getProject() - if project is None: - return {"error": "No project open"} - - try: - binary_file = File(binary_path) - if not binary_file.exists(): - return {"error": "Binary file not found: " + binary_path} - - monitor = ConsoleTaskMonitor() - log = MessageLog() - - # Use project as the consumer for domain object lifecycle - consumer = project - - # Ghidra 12+ API: importByUsingBestGuess(File, Project, String folderPath, Object consumer, MessageLog, TaskMonitor) - load_results = AutoImporter.importByUsingBestGuess( - binary_file, - project, - "/", - consumer, - log, - monitor - ) - - if load_results is None: - return {"error": "Failed to import binary"} - - # Save each loaded program to the project, then release - for loaded in load_results: - loaded.save(monitor) - load_results.release(consumer) - - return {"status": "success", "program": program_name} - - except Exception as e: - return {"error": "Import failed: " + str(e)} - -def handle_analyze(args): - """Trigger auto-analysis on the current program (blocking).""" - from ghidra.util.task import ConsoleTaskMonitor - import time - - program_name = args.get("program") - if not program_name: - return {"error": "No program name provided"} - - if currentProgram is None: - return {"error": "No program currently loaded"} - - # If requested program differs from current, switch to it first - if currentProgram.getName() != program_name: - switch_result = handle_open_program({"program": program_name}) - if "error" in switch_result: - return switch_result - - try: - # Try Ghidra 12+ import path first, fall back to older path - try: - from ghidra.app.plugin.core.analysis import AutoAnalysisManager - except ImportError: - from ghidra.app.cmd.analysis import AutoAnalysisManager - - monitor = ConsoleTaskMonitor() - auto_mgr = AutoAnalysisManager.getAnalysisManager(currentProgram) - - if auto_mgr is None: - return {"error": "Could not get AutoAnalysisManager"} - - # Schedule full re-analysis - auto_mgr.reAnalyzeAll(None) - auto_mgr.startAnalysis(monitor) - - # startAnalysis() is non-blocking — poll until analysis completes - while auto_mgr.isAnalyzing(): - time.sleep(1) - - # Save the program so analysis results persist - try: - currentProgram.save("Analysis complete", monitor) - except Exception as save_err: - # Best effort — some contexts don't allow save - pass - - return {"status": "success", "program": program_name} - - except Exception as e: - return {"error": "Analysis failed: " + str(e)} - -def handle_list_programs(args): - """List all programs in the current project.""" - project = state.getProject() - if project is None: - return {"error": "No project open"} - - try: - project_data = project.getProjectData() - root_folder = project_data.getRootFolder() - programs = [] - - for domain_file in root_folder.getFiles(): - is_current = (currentProgram is not None and - domain_file.getName() == currentProgram.getName()) - programs.append({ - "name": domain_file.getName(), - "path": domain_file.getPathname(), - "type": domain_file.getContentType(), - "version": domain_file.getVersion(), - "current": is_current, - }) - - return {"programs": programs, "count": len(programs)} - - except Exception as e: - return {"error": "Failed to list programs: " + str(e)} - -def handle_open_program(args): - """Open/switch to a program in the current project.""" - global currentProgram - from ghidra.util.task import ConsoleTaskMonitor - - program_name = args.get("program") - if not program_name: - return {"error": "Program name required"} - - # Already the current program? No-op. - if currentProgram is not None and currentProgram.getName() == program_name: - return {"status": "success", "program": program_name} - - project = state.getProject() - if project is None: - return {"error": "No project open"} - - try: - project_data = project.getProjectData() - - # Find the domain file by name (search root folder) - domain_file = None - root_folder = project_data.getRootFolder() - for f in root_folder.getFiles(): - if f.getName() == program_name: - domain_file = f - break - - if domain_file is None: - # Try as a path (must start with /) - path = program_name if program_name.startswith("/") else "/" + program_name - domain_file = project_data.getFile(path) - - if domain_file is None: - # List available programs for a helpful error message - available = [f.getName() for f in root_folder.getFiles()] - return {"error": "Program not found: " + program_name + ". Available: " + ", ".join(available)} - - # Use a stable consumer object for domain object lifecycle - consumer = project - - # Release current program if one is open - if currentProgram is not None: - try: - currentProgram.save("Auto-save before switch", ConsoleTaskMonitor()) - except: - pass # Best effort save - try: - currentProgram.release(consumer) - except: - pass - - # Open the requested program - monitor = ConsoleTaskMonitor() - program = domain_file.getDomainObject( - consumer, - True, # upgrade if needed - False, # don't recover - monitor - ) - - # Update globals - currentProgram = program - builtins.currentProgram = program - - return { - "status": "success", - "program": program.getName(), - } - - except Exception as e: - return {"error": "Failed to open program: " + str(e)} - -COMMANDS = { - "ping": handle_ping, - # Import/Analyze commands - "import": handle_import, - "analyze": handle_analyze, - "program_info": handle_program_info, - "program_close": handle_program_close, - "program_delete": handle_program_delete, - "program_export": handle_program_export, - "list_programs": handle_list_programs, - "open_program": handle_open_program, - "list_functions": handle_list_functions, - "decompile": handle_decompile, - "list_strings": handle_list_strings, - "list_imports": handle_list_imports, - "list_exports": handle_list_exports, - "memory_map": handle_memory_map, - "xrefs_to": handle_xrefs_to, - "xrefs_from": handle_xrefs_from, - "find_string": handle_find_string, - "find_bytes": handle_find_bytes, - "find_function": handle_find_function, - "find_calls": handle_find_calls, - "find_crypto": handle_find_crypto, - "find_interesting": handle_find_interesting, - "script_run": handle_script_run, - "script_python": handle_script_python, - "script_java": handle_script_java, - "script_list": handle_script_list, - # Symbol commands - "symbol_list": handle_symbol_list, - "symbol_get": handle_symbol_get, - "symbol_create": handle_symbol_create, - "symbol_delete": handle_symbol_delete, - "symbol_rename": handle_symbol_rename, - # Type commands - "type_list": handle_type_list, - "type_get": handle_type_get, - "type_create": handle_type_create, - "type_apply": handle_type_apply, - # Comment commands - "comment_list": handle_comment_list, - "comment_get": handle_comment_get, - "comment_set": handle_comment_set, - "comment_delete": handle_comment_delete, - # Graph commands - "graph_calls": handle_graph_calls, - "graph_callers": handle_graph_callers, - "graph_callees": handle_graph_callees, - "graph_export": handle_graph_export, - # Diff commands - "diff_programs": handle_diff_programs, - "diff_functions": handle_diff_functions, - # Patch commands - "patch_bytes": handle_patch_bytes, - "patch_nop": handle_patch_nop, - "patch_export": handle_patch_export, - # Other commands - "disasm": handle_disasm, - "stats": handle_stats, -} - -# --- Server Logic --- - -def handle_request(line): - """Parse and handle a single JSON request.""" - try: - req = json.loads(line) - cmd = req.get("command") - args = req.get("args", {}) - - if cmd == "shutdown": - return {"status": "shutdown"}, True - - if cmd in COMMANDS: - result = COMMANDS[cmd](args) - if "error" in result: - return {"status": "error", "message": result["error"]}, False - return {"status": "success", "data": result}, False - else: - return {"status": "error", "message": "Unknown command: " + str(cmd)}, False - - except Exception as e: - return {"status": "error", "message": str(e)}, False - -def start_server(port=BRIDGE_PORT): - """Start the bridge server.""" - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(('127.0.0.1', port)) - s.listen(1) - - # Signal ready to the parent process - print("---GHIDRA_CLI_START---") - print(json.dumps({"status": "ready", "port": port})) - print("---GHIDRA_CLI_END---") - - running = True - while running: - try: - conn, addr = s.accept() - f = conn.makefile('r') - out = conn.makefile('w') - - try: - while True: - line = f.readline() - if not line: - break - - response, should_shutdown = handle_request(line.strip()) - out.write(json.dumps(response) + "\n") - out.flush() - - if should_shutdown: - running = False - break - finally: - f.close() - out.close() - conn.close() - - except Exception as e: - print("Bridge error: " + str(e)) - - s.close() - -# --- Entry Point --- - -def is_headless_mode(): - """Check if running in headless mode (via analyzeHeadless or pyghidraRun --headless).""" - # Check Ghidra's built-in function (available in GhidraScript context) - try: - # isRunningHeadless is injected by Ghidra into script namespace - if isRunningHeadless(): - return True - except NameError: - pass - - # PyGhidra injects getScriptArgs() instead of args variable - try: - script_args = getScriptArgs() - if script_args is not None: - return True # If we can get script args, we're running as a Ghidra script - except NameError: - pass - - # Fallback: check environment - headless mode typically has no display - import os - if os.environ.get('DISPLAY') is None and os.environ.get('WAYLAND_DISPLAY') is None: - return True - - return False - -if __name__ == "__main__" or True: # Also runs when sourced by Ghidra - # Determine port from args if provided - port = BRIDGE_PORT - if 'args' in dir() and len(args) > 0: - try: - port = int(args[0]) - except: - pass - - # If running headless, block on server (keeps process alive) - # Otherwise, run in background thread for GUI mode - if is_headless_mode(): - start_server(port) - else: - # GUI mode - run in background thread to not freeze UI - t = threading.Thread(target=start_server, args=(port,)) - t.daemon = True - t.start() - print("Bridge started in background on port " + str(port)) diff --git a/src/ghidra/scripts/comments.py b/src/ghidra/scripts/comments.py deleted file mode 100644 index 9c3acb9..0000000 --- a/src/ghidra/scripts/comments.py +++ /dev/null @@ -1,170 +0,0 @@ -# Comment operations script -# @category CLI - -import sys -import json - -def list_comments(): - """List all comments in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - listing = currentProgram.getListing() - comments = [] - - # Iterate over all memory blocks to handle multiple address spaces - from ghidra.program.model.address import AddressSet - memory = currentProgram.getMemory() - - for block in memory.getBlocks(): - # Create an AddressSet for this block - address_set = AddressSet(block.getStart(), block.getEnd()) - - # Get comment addresses in this block - code_unit_iter = listing.getCommentAddressIterator(address_set, True) - - for addr in code_unit_iter: - code_unit = listing.getCodeUnitAt(addr) - if code_unit is None: - continue - - from ghidra.program.model.listing import CodeUnit - - comment_types = [ - ("EOL", CodeUnit.EOL_COMMENT), - ("PRE", CodeUnit.PRE_COMMENT), - ("POST", CodeUnit.POST_COMMENT), - ("PLATE", CodeUnit.PLATE_COMMENT) - ] - - for comment_name, comment_type in comment_types: - text = code_unit.getComment(comment_type) - if text: - comments.append({ - "address": str(addr), - "type": comment_name, - "text": text - }) - - return {"comments": comments, "count": len(comments)} - -def get_comments(address_str): - """Get comments at a specific address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - listing = currentProgram.getListing() - code_unit = listing.getCodeUnitAt(addr) - - if code_unit is None: - return {"error": "No code unit at address: " + address_str} - - from ghidra.program.model.listing import CodeUnit - - comments = [] - comment_types = [ - ("EOL", CodeUnit.EOL_COMMENT), - ("PRE", CodeUnit.PRE_COMMENT), - ("POST", CodeUnit.POST_COMMENT), - ("PLATE", CodeUnit.PLATE_COMMENT) - ] - - for comment_name, comment_type in comment_types: - text = code_unit.getComment(comment_type) - if text: - comments.append({ - "type": comment_name, - "text": text - }) - - return {"address": address_str, "comments": comments} - except Exception as e: - return {"error": "Failed to get comments: " + str(e)} - -def set_comment(address_str, text, comment_type_str): - """Set a comment at a specific address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - listing = currentProgram.getListing() - from ghidra.program.model.listing import CodeUnit - - valid_types = {"EOL", "PRE", "POST", "PLATE"} - if comment_type_str not in valid_types: - return {"error": "Invalid comment type: " + comment_type_str + ". Must be one of: EOL, PRE, POST, PLATE"} - - comment_type = CodeUnit.EOL_COMMENT - if comment_type_str == "PRE": - comment_type = CodeUnit.PRE_COMMENT - elif comment_type_str == "POST": - comment_type = CodeUnit.POST_COMMENT - elif comment_type_str == "PLATE": - comment_type = CodeUnit.PLATE_COMMENT - - listing.setComment(addr, comment_type, text) - return {"status": "set", "address": address_str} - except Exception as e: - return {"error": "Failed to set comment: " + str(e)} - -def delete_comment(address_str): - """Delete all comments at a specific address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - listing = currentProgram.getListing() - from ghidra.program.model.listing import CodeUnit - - listing.setComment(addr, CodeUnit.EOL_COMMENT, None) - listing.setComment(addr, CodeUnit.PRE_COMMENT, None) - listing.setComment(addr, CodeUnit.POST_COMMENT, None) - listing.setComment(addr, CodeUnit.PLATE_COMMENT, None) - - return {"status": "deleted", "address": address_str} - except Exception as e: - return {"error": "Failed to delete comment: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "list": - result = list_comments() - elif command == "get": - result = get_comments(args[1] if len(args) > 1 else None) - elif command == "set": - text = args[2] if len(args) > 2 else "" - comment_type = args[3] if len(args) > 3 else "EOL" - result = set_comment(args[1] if len(args) > 1 else None, text, comment_type) - elif command == "delete": - result = delete_comment(args[1] if len(args) > 1 else None) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/diff.py b/src/ghidra/scripts/diff.py deleted file mode 100644 index 340bea1..0000000 --- a/src/ghidra/scripts/diff.py +++ /dev/null @@ -1,132 +0,0 @@ -# Diff operations script -# @category CLI - -import sys -import json - -def diff_programs(prog1, prog2): - """Compare two programs structurally.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - func_manager = currentProgram.getFunctionManager() - memory = currentProgram.getMemory() - symbol_table = currentProgram.getSymbolTable() - - prog1_stats = { - "name": prog1, - "function_count": func_manager.getFunctionCount(), - "memory_size": memory.getSize(), - "symbol_count": symbol_table.getNumSymbols() - } - - memory_blocks = [] - for block in memory.getBlocks(): - memory_blocks.append({ - "name": block.getName(), - "start": str(block.getStart()), - "end": str(block.getEnd()), - "size": block.getSize() - }) - - prog1_stats["memory_blocks"] = memory_blocks - - return { - "program1": prog1_stats, - "program2": {"name": prog2, "note": "Comparison requires loading second program"}, - "status": "partial", - "message": "Single program stats returned (multi-program comparison not implemented)" - } - except Exception as e: - return {"error": "Failed to diff programs: " + str(e)} - -def diff_functions(func1, func2): - """Compare two functions by decompilation.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - from ghidra.app.decompiler import DecompInterface - - func_manager = currentProgram.getFunctionManager() - - target_func1 = None - target_func2 = None - - for func in func_manager.getFunctions(True): - if func.getName() == func1: - target_func1 = func - if func.getName() == func2: - target_func2 = func - - if target_func1 is None: - return {"error": "Function not found: " + func1} - if target_func2 is None: - return {"error": "Function not found: " + func2} - - decompiler = DecompInterface() - decompiler.openProgram(currentProgram) - - result1 = decompiler.decompileFunction(target_func1, 30, monitor) - result2 = decompiler.decompileFunction(target_func2, 30, monitor) - - if not result1.decompileCompleted(): - return {"error": "Failed to decompile " + func1} - if not result2.decompileCompleted(): - return {"error": "Failed to decompile " + func2} - - code1 = result1.getDecompiledFunction().getC() - code2 = result2.getDecompiledFunction().getC() - - lines1 = code1.split('\n') - lines2 = code2.split('\n') - - diff_lines = [] - max_lines = max(len(lines1), len(lines2)) - - for i in range(max_lines): - line1 = lines1[i] if i < len(lines1) else "" - line2 = lines2[i] if i < len(lines2) else "" - - if line1 != line2: - diff_lines.append({ - "line": i + 1, - "func1": line1, - "func2": line2, - "status": "changed" - }) - - return { - "func1": {"name": func1, "lines": len(lines1), "code": code1}, - "func2": {"name": func2, "lines": len(lines2), "code": code2}, - "differences": diff_lines, - "diff_count": len(diff_lines) - } - except Exception as e: - return {"error": "Failed to diff functions: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "diff_programs": - result = diff_programs(args[1] if len(args) > 1 else "", args[2] if len(args) > 2 else "") - elif command == "diff_functions": - result = diff_functions(args[1] if len(args) > 1 else "", args[2] if len(args) > 2 else "") - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/disasm.py b/src/ghidra/scripts/disasm.py deleted file mode 100644 index c7a6b54..0000000 --- a/src/ghidra/scripts/disasm.py +++ /dev/null @@ -1,86 +0,0 @@ -# Disassembly script -# @category CLI - -import sys -import json - -def disassemble(address_str, count): - """Disassemble instructions starting at address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr_factory = currentProgram.getAddressFactory() - - if address_str.startswith("0x") or address_str.startswith("0X"): - address_str = address_str[2:] - - addr = addr_factory.getAddress(address_str) - - if addr is None: - return {"error": "Invalid address: " + address_str} - - listing = currentProgram.getListing() - instruction = listing.getInstructionAt(addr) - - if instruction is None: - return {"error": "No instruction at address: " + address_str} - - results = [] - current_instr = instruction - - for i in range(count): - if current_instr is None: - break - - instr_addr = current_instr.getAddress() - - byte_array = current_instr.getBytes() - bytes_hex = "" - for b in byte_array: - bytes_hex += "{:02x}".format(b & 0xff) - - mnemonic = current_instr.getMnemonicString() - - operands = [] - num_operands = current_instr.getNumOperands() - for j in range(num_operands): - operands.append(str(current_instr.getDefaultOperandRepresentation(j))) - - results.append({ - "address": str(instr_addr), - "bytes": bytes_hex, - "mnemonic": mnemonic, - "operands": operands - }) - - current_instr = current_instr.getNext() - - return {"results": results, "count": len(results)} - except Exception as e: - return {"error": "Failed to disassemble: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "disasm": - address = args[1] if len(args) > 1 else "0x0" - count = int(args[2]) if len(args) > 2 else 10 - result = disassemble(address, count) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/find.py b/src/ghidra/scripts/find.py deleted file mode 100644 index e5bec06..0000000 --- a/src/ghidra/scripts/find.py +++ /dev/null @@ -1,259 +0,0 @@ -# Find/search operations script -# @category CLI - -import sys -import json - -def find_strings(pattern): - """Find string references matching pattern.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - listing = currentProgram.getListing() - results = [] - - data_iter = listing.getDefinedData(True) - while data_iter.hasNext(): - data = data_iter.next() - if data.hasStringValue(): - string_val = str(data.getValue()) - if pattern.lower() in string_val.lower(): - results.append({ - "address": str(data.getAddress()), - "value": string_val, - "length": data.getLength() - }) - - return {"results": results, "count": len(results)} - except Exception as e: - return {"error": "Failed to find strings: " + str(e)} - -def find_bytes(hex_pattern): - """Find byte patterns in memory.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - hex_clean = hex_pattern.replace("0x", "").replace(" ", "") - - byte_array = [] - for i in range(0, len(hex_clean), 2): - byte_val = int(hex_clean[i:i+2], 16) - if byte_val > 127: - byte_val = byte_val - 256 - byte_array.append(byte_val) - - from java.lang import Byte - search_bytes = [Byte(b) for b in byte_array] - - memory = currentProgram.getMemory() - results = [] - - addr = memory.getMinAddress() - while addr is not None: - found_addr = memory.findBytes(addr, search_bytes, None, True, monitor) - if found_addr is None: - break - results.append({"address": str(found_addr)}) - addr = found_addr.add(1) - if len(results) >= 100: - break - - return {"results": results, "count": len(results)} - except Exception as e: - return {"error": "Failed to find bytes: " + str(e)} - -def find_functions(pattern): - """Find functions matching name pattern.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - func_manager = currentProgram.getFunctionManager() - results = [] - - for func in func_manager.getFunctions(True): - func_name = func.getName() - - if "*" in pattern: - import fnmatch - if fnmatch.fnmatch(func_name, pattern): - results.append({ - "name": func_name, - "address": str(func.getEntryPoint()), - "size": func.getBody().getNumAddresses() - }) - elif pattern.lower() in func_name.lower(): - results.append({ - "name": func_name, - "address": str(func.getEntryPoint()), - "size": func.getBody().getNumAddresses() - }) - - return {"results": results, "count": len(results)} - except Exception as e: - return {"error": "Failed to find functions: " + str(e)} - -def find_calls(func_name): - """Find all calls to a specific function.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - func_manager = currentProgram.getFunctionManager() - target_func = None - - for func in func_manager.getFunctions(True): - if func.getName() == func_name: - target_func = func - break - - if target_func is None: - return {"error": "Function not found: " + func_name} - - ref_manager = currentProgram.getReferenceManager() - target_addr = target_func.getEntryPoint() - refs = ref_manager.getReferencesTo(target_addr) - - results = [] - for ref in refs: - if ref.getReferenceType().isCall(): - from_addr = ref.getFromAddress() - from_func = func_manager.getFunctionContaining(from_addr) - - caller_name = "unknown" - if from_func is not None: - caller_name = from_func.getName() - - results.append({ - "address": str(from_addr), - "caller": caller_name, - "type": str(ref.getReferenceType()) - }) - - return {"results": results, "count": len(results), "target": func_name} - except Exception as e: - return {"error": "Failed to find calls: " + str(e)} - -def find_crypto(): - """Find potential crypto constants.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - memory = currentProgram.getMemory() - results = [] - - crypto_patterns = { - "AES S-box": "637c777bf26b6fc53001672bfed7ab76", - "SHA-256": "428a2f98d728ae227137449123ef65cd", - "MD5": "d76aa478e8c7b756242070db01234567", - } - - for name, pattern in crypto_patterns.items(): - hex_clean = pattern.replace(" ", "") - byte_array = [] - - for i in range(0, len(hex_clean), 2): - byte_val = int(hex_clean[i:i+2], 16) - if byte_val > 127: - byte_val = byte_val - 256 - byte_array.append(byte_val) - - from java.lang import Byte - search_bytes = [Byte(b) for b in byte_array] - - addr = memory.getMinAddress() - found_addr = memory.findBytes(addr, search_bytes, None, True, monitor) - - if found_addr is not None: - results.append({ - "type": name, - "address": str(found_addr), - "pattern": pattern - }) - - return {"results": results, "count": len(results)} - except Exception as e: - return {"error": "Failed to find crypto: " + str(e)} - -def find_interesting(): - """Find interesting functions using heuristics.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - func_manager = currentProgram.getFunctionManager() - ref_manager = currentProgram.getReferenceManager() - results = [] - - suspicious_names = ["password", "key", "encrypt", "decrypt", "crypt", "auth", "login", "admin", "secret"] - - for func in func_manager.getFunctions(True): - func_name = func.getName() - func_addr = func.getEntryPoint() - func_size = func.getBody().getNumAddresses() - - xref_count = len(list(ref_manager.getReferencesTo(func_addr))) - - reasons = [] - - if func_size > 1000: - reasons.append("large function ({} bytes)".format(func_size)) - - if xref_count > 50: - reasons.append("many xrefs ({})".format(xref_count)) - - for sus_name in suspicious_names: - if sus_name in func_name.lower(): - reasons.append("suspicious name") - break - - if reasons: - results.append({ - "name": func_name, - "address": str(func_addr), - "size": func_size, - "xrefs": xref_count, - "reasons": reasons - }) - - results.sort(key=lambda x: len(x["reasons"]), reverse=True) - - return {"results": results[:50], "count": len(results)} - except Exception as e: - return {"error": "Failed to find interesting functions: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "find_string": - result = find_strings(args[1] if len(args) > 1 else "") - elif command == "find_bytes": - result = find_bytes(args[1] if len(args) > 1 else "") - elif command == "find_function": - result = find_functions(args[1] if len(args) > 1 else "") - elif command == "find_calls": - result = find_calls(args[1] if len(args) > 1 else "") - elif command == "find_crypto": - result = find_crypto() - elif command == "find_interesting": - result = find_interesting() - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/graph.py b/src/ghidra/scripts/graph.py deleted file mode 100644 index 1151ab2..0000000 --- a/src/ghidra/scripts/graph.py +++ /dev/null @@ -1,223 +0,0 @@ -# Graph operations script -# @category CLI - -import sys -import json - -def get_call_graph(limit): - """Build full call graph.""" - if currentProgram is None: - return {"error": "No program loaded"} - - function_manager = currentProgram.getFunctionManager() - reference_manager = currentProgram.getReferenceManager() - - nodes = [] - edges = [] - count = 0 - - for func in function_manager.getFunctions(True): - if limit and count >= limit: - break - - func_addr = str(func.getEntryPoint()) - nodes.append({ - "id": func_addr, - "name": func.getName(), - "address": func_addr - }) - - from ghidra.program.model.symbol import RefType - refs = reference_manager.getReferencesFrom(func.getEntryPoint()) - for ref in refs: - if ref.getReferenceType().isCall(): - target_addr = ref.getToAddress() - target_func = function_manager.getFunctionAt(target_addr) - if target_func: - edges.append({ - "from": func_addr, - "to": str(target_addr), - "type": "call" - }) - - count += 1 - - return {"nodes": nodes, "edges": edges, "node_count": len(nodes), "edge_count": len(edges)} - -def get_callers(function_name, depth): - """Get functions that call the specified function.""" - if currentProgram is None: - return {"error": "No program loaded"} - - function_manager = currentProgram.getFunctionManager() - reference_manager = currentProgram.getReferenceManager() - - target_func = None - if function_name.startswith("0x") or all(c in "0123456789abcdefABCDEF" for c in function_name): - addr = currentProgram.getAddressFactory().getAddress(function_name) - if addr: - target_func = function_manager.getFunctionAt(addr) - else: - for func in function_manager.getFunctions(True): - if func.getName() == function_name: - target_func = func - break - - if not target_func: - return {"error": "Function not found: " + function_name} - - callers = [] - visited = set() - - def find_callers(func, current_depth): - if depth and current_depth >= depth: - return - if str(func.getEntryPoint()) in visited: - return - - visited.add(str(func.getEntryPoint())) - - from ghidra.program.model.symbol import RefType - refs = reference_manager.getReferencesTo(func.getEntryPoint()) - - for ref in refs: - if ref.getReferenceType().isCall(): - from_addr = ref.getFromAddress() - caller_func = function_manager.getFunctionContaining(from_addr) - if caller_func: - caller_info = { - "name": caller_func.getName(), - "address": str(caller_func.getEntryPoint()), - "call_site": str(from_addr), - "depth": current_depth - } - callers.append(caller_info) - - if depth is None or current_depth + 1 < depth: - find_callers(caller_func, current_depth + 1) - - find_callers(target_func, 0) - - return {"function": function_name, "callers": callers, "count": len(callers)} - -def get_callees(function_name, depth): - """Get functions called by the specified function.""" - if currentProgram is None: - return {"error": "No program loaded"} - - function_manager = currentProgram.getFunctionManager() - reference_manager = currentProgram.getReferenceManager() - - target_func = None - if function_name.startswith("0x") or all(c in "0123456789abcdefABCDEF" for c in function_name): - addr = currentProgram.getAddressFactory().getAddress(function_name) - if addr: - target_func = function_manager.getFunctionAt(addr) - else: - for func in function_manager.getFunctions(True): - if func.getName() == function_name: - target_func = func - break - - if not target_func: - return {"error": "Function not found: " + function_name} - - callees = [] - visited = set() - - def find_callees(func, current_depth): - if depth and current_depth >= depth: - return - if str(func.getEntryPoint()) in visited: - return - - visited.add(str(func.getEntryPoint())) - - from ghidra.program.model.symbol import RefType - refs = reference_manager.getReferencesFrom(func.getEntryPoint()) - - for ref in refs: - if ref.getReferenceType().isCall(): - to_addr = ref.getToAddress() - callee_func = function_manager.getFunctionAt(to_addr) - if callee_func: - callee_info = { - "name": callee_func.getName(), - "address": str(callee_func.getEntryPoint()), - "call_site": str(ref.getFromAddress()), - "depth": current_depth - } - callees.append(callee_info) - - if depth is None or current_depth + 1 < depth: - find_callees(callee_func, current_depth + 1) - - find_callees(target_func, 0) - - return {"function": function_name, "callees": callees, "count": len(callees)} - -def export_graph(export_format): - """Export call graph in specified format.""" - if currentProgram is None: - return {"error": "No program loaded"} - - graph_data = get_call_graph(None) - if "error" in graph_data: - return graph_data - - if export_format == "json": - return graph_data - elif export_format == "dot": - lines = ["digraph CallGraph {"] - lines.append(' rankdir=LR;') - lines.append(' node [shape=box];') - - for node in graph_data["nodes"]: - node_id = node["id"].replace(":", "_") - label = node["name"] - lines.append(' "{}" [label="{}"];'.format(node_id, label)) - - for edge in graph_data["edges"]: - from_id = edge["from"].replace(":", "_") - to_id = edge["to"].replace(":", "_") - lines.append(' "{}" -> "{}";'.format(from_id, to_id)) - - lines.append("}") - return {"format": "dot", "output": "\n".join(lines)} - else: - return {"error": "Unsupported format: " + export_format} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "calls": - limit = int(args[1]) if len(args) > 1 and args[1] else None - result = get_call_graph(limit) - elif command == "callers": - func_name = args[1] if len(args) > 1 else None - depth = int(args[2]) if len(args) > 2 and args[2] else None - result = get_callers(func_name, depth) - elif command == "callees": - func_name = args[1] if len(args) > 1 else None - depth = int(args[2]) if len(args) > 2 and args[2] else None - result = get_callees(func_name, depth) - elif command == "export": - fmt = args[1] if len(args) > 1 else "json" - result = export_graph(fmt) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/patch.py b/src/ghidra/scripts/patch.py deleted file mode 100644 index 8783a6a..0000000 --- a/src/ghidra/scripts/patch.py +++ /dev/null @@ -1,134 +0,0 @@ -# Patch operations script -# @category CLI - -import sys -import json - -def patch_bytes(address_str, hex_data): - """Patch bytes at the specified address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - hex_clean = hex_data.replace("0x", "").replace(" ", "") - - byte_array = [] - for i in range(0, len(hex_clean), 2): - byte_val = int(hex_clean[i:i+2], 16) - if byte_val > 127: - byte_val = byte_val - 256 - byte_array.append(byte_val) - - from java.lang import Byte - patch_bytes = [Byte(b) for b in byte_array] - - memory = currentProgram.getMemory() - memory.setBytes(addr, patch_bytes) - - return { - "status": "patched", - "address": str(addr), - "bytes": len(patch_bytes) - } - except Exception as e: - return {"error": "Failed to patch bytes: " + str(e)} - -def patch_nop(address_str): - """NOP out instruction at the specified address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - listing = currentProgram.getListing() - instruction = listing.getInstructionAt(addr) - - if instruction is None: - return {"error": "No instruction at address: " + address_str} - - instr_length = instruction.getLength() - - processor = currentProgram.getLanguage().getProcessor().toString() - - if "x86" in processor.lower(): - nop_byte = 0x90 - elif "ARM" in processor or "aarch" in processor.lower(): - nop_byte = 0x00 - else: - nop_byte = 0x00 - - from java.lang import Byte - nop_bytes = [Byte(nop_byte) for _ in range(instr_length)] - - memory = currentProgram.getMemory() - memory.setBytes(addr, nop_bytes) - - return { - "status": "nopped", - "address": str(addr), - "bytes": instr_length - } - except Exception as e: - return {"error": "Failed to NOP instruction: " + str(e)} - -def export_binary(output_path): - """Export the patched binary.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - from ghidra.app.util.exporter import BinaryExporter - from java.io import File - - exporter = BinaryExporter() - output_file = File(output_path) - - exporter.export(output_file, currentProgram, None, monitor) - - return { - "status": "exported", - "output": output_path - } - except Exception as e: - return {"error": "Failed to export binary: " + str(e)} - -# Alias for bridge.py compatibility -def export_patches(output_path): - """Export patches (alias for export_binary).""" - return export_binary(output_path) - -if __name__ == "__main__": - try: - args = getScriptArgs() - - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "patch_bytes": - result = patch_bytes(args[1] if len(args) > 1 else "", args[2] if len(args) > 2 else "") - elif command == "patch_nop": - result = patch_nop(args[1] if len(args) > 1 else "") - elif command == "patch_export": - result = export_binary(args[1] if len(args) > 1 else "") - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/program.py b/src/ghidra/scripts/program.py deleted file mode 100644 index 629d458..0000000 --- a/src/ghidra/scripts/program.py +++ /dev/null @@ -1,115 +0,0 @@ -# Program operations script -# @category CLI - -import sys -import json - -def close_program(): - """Close the current program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - program_name = currentProgram.getName() - state.getTool().closeProgram(currentProgram, False) - - return {"status": "closed", "program": program_name} - -def delete_program(program_name): - """Delete a program from the project.""" - project = state.getProject() - if project is None: - return {"error": "No project open"} - - project_data = project.getProjectData() - - try: - program_file = project_data.getFile(program_name) - if program_file is None: - return {"error": "Program not found: " + program_name} - - project_data.deleteFile(program_name) - return {"status": "deleted", "program": program_name} - except Exception as e: - return {"error": "Failed to delete program: " + str(e)} - -def get_program_info(): - """Get current program metadata.""" - if currentProgram is None: - return {"error": "No program loaded"} - - info = { - "name": currentProgram.getName(), - "path": currentProgram.getExecutablePath(), - "format": currentProgram.getExecutableFormat(), - "processor": str(currentProgram.getLanguage().getProcessor()), - "language": str(currentProgram.getLanguage()), - "compiler": currentProgram.getCompiler() if currentProgram.getCompiler() else None, - "image_base": str(currentProgram.getImageBase()), - "min_address": str(currentProgram.getMinAddress()), - "max_address": str(currentProgram.getMaxAddress()), - "creation_date": str(currentProgram.getCreationDate()) - } - - return info - -def export_program(export_format, output_path): - """Export program to specified format.""" - if currentProgram is None: - return {"error": "No program loaded"} - - from ghidra.app.util.exporter import Exporter - from ghidra.framework.model import DomainFile - from java.io import File - - if export_format == "json": - data = get_program_info() - - function_manager = currentProgram.getFunctionManager() - functions = [] - for func in function_manager.getFunctions(True): - functions.append({ - "name": func.getName(), - "address": str(func.getEntryPoint()), - "size": func.getBody().getNumAddresses() - }) - data["functions"] = functions - - if output_path: - with open(output_path, 'w') as f: - json.dump(data, f, indent=2) - return {"status": "exported", "format": "json", "output": output_path} - else: - return data - else: - return {"error": "Unsupported export format: " + export_format} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "close": - result = close_program() - elif command == "delete": - result = delete_program(args[1] if len(args) > 1 else None) - elif command == "info": - result = get_program_info() - elif command == "export": - fmt = args[1] if len(args) > 1 else "json" - output = args[2] if len(args) > 2 else None - result = export_program(fmt, output) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/script_runner.py b/src/ghidra/scripts/script_runner.py deleted file mode 100644 index 389e3fc..0000000 --- a/src/ghidra/scripts/script_runner.py +++ /dev/null @@ -1,121 +0,0 @@ -# Script execution operations -# @category CLI - -import sys -import json -import os - -def run_script(script_path, script_args): - """Run a user script file.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - if not os.path.exists(script_path): - return {"error": "Script not found: " + script_path} - - from ghidra.app.script import GhidraScriptUtil - - script_info = GhidraScriptUtil.findScriptByName(os.path.basename(script_path)) - if script_info is None: - return {"error": "Could not load script: " + script_path} - - result = runScript(script_path, script_args if script_args else []) - - return { - "status": "executed", - "script": script_path, - "result": str(result) if result is not None else None - } - except Exception as e: - return {"error": "Failed to run script: " + str(e)} - -def exec_python(code): - """Execute inline Python code.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - local_vars = { - "currentProgram": currentProgram, - "currentAddress": currentAddress if 'currentAddress' in dir() else None, - "currentLocation": currentLocation if 'currentLocation' in dir() else None, - "state": state if 'state' in dir() else None - } - - exec(code, globals(), local_vars) - - output = local_vars.get("output", None) - - return { - "status": "executed", - "output": str(output) if output is not None else "Code executed successfully" - } - except Exception as e: - return {"error": "Failed to execute Python code: " + str(e)} - -def exec_java(code): - """Execute inline Java code.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - return {"error": "Java execution not yet implemented"} - except Exception as e: - return {"error": "Failed to execute Java code: " + str(e)} - -def list_scripts(): - """List available scripts.""" - try: - from ghidra.app.script import GhidraScriptUtil - - script_infos = GhidraScriptUtil.getScriptSourceDirectories() - scripts = [] - - for script_dir in script_infos: - script_path = str(script_dir) - if os.path.exists(script_path) and os.path.isdir(script_path): - for filename in os.listdir(script_path): - if filename.endswith('.py') or filename.endswith('.java'): - scripts.append({ - "name": filename, - "path": os.path.join(script_path, filename), - "type": "python" if filename.endswith('.py') else "java" - }) - - return {"scripts": scripts, "count": len(scripts)} - except Exception as e: - return {"error": "Failed to list scripts: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "run": - script_path = args[1] if len(args) > 1 else None - script_args = args[2:] if len(args) > 2 else [] - result = run_script(script_path, script_args) - elif command == "python": - code = args[1] if len(args) > 1 else None - result = exec_python(code) - elif command == "java": - code = args[1] if len(args) > 1 else None - result = exec_java(code) - elif command == "list": - result = list_scripts() - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/stats.py b/src/ghidra/scripts/stats.py deleted file mode 100644 index 2bccda9..0000000 --- a/src/ghidra/scripts/stats.py +++ /dev/null @@ -1,98 +0,0 @@ -# Program statistics script -# @category CLI - -import sys -import json - -def get_stats(): - """Gather comprehensive program statistics.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - func_manager = currentProgram.getFunctionManager() - symbol_table = currentProgram.getSymbolTable() - memory = currentProgram.getMemory() - data_type_manager = currentProgram.getDataTypeManager() - listing = currentProgram.getListing() - - function_count = func_manager.getFunctionCount() - - symbol_count = 0 - symbol_iter = symbol_table.getAllSymbols(True) - while symbol_iter.hasNext(): - symbol_iter.next() - symbol_count += 1 - - string_count = 0 - data_iter = listing.getDefinedData(True) - while data_iter.hasNext(): - data = data_iter.next() - if data.hasStringValue(): - string_count += 1 - - memory_size = 0 - for block in memory.getBlocks(): - memory_size += block.getSize() - - section_count = len(list(memory.getBlocks())) - - import_count = 0 - export_count = 0 - for symbol in symbol_table.getExternalSymbols(): - import_count += 1 - - export_iter = symbol_table.getExternalEntryPointIterator() - while export_iter.hasNext(): - export_iter.next() - export_count += 1 - - data_type_count = data_type_manager.getDataTypeCount(False) - - instruction_count = 0 - code_unit_iter = listing.getInstructions(True) - while code_unit_iter.hasNext(): - code_unit_iter.next() - instruction_count += 1 - - stats = { - "functions": function_count, - "symbols": symbol_count, - "strings": string_count, - "imports": import_count, - "exports": export_count, - "memory_size": memory_size, - "sections": section_count, - "data_types": data_type_count, - "instructions": instruction_count, - "program_name": currentProgram.getName(), - "executable_format": currentProgram.getExecutableFormat(), - "compiler": str(currentProgram.getCompiler()) if currentProgram.getCompiler() else "Unknown" - } - - return {"stats": stats} - except Exception as e: - return {"error": "Failed to gather statistics: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "stats": - result = get_stats() - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/symbols.py b/src/ghidra/scripts/symbols.py deleted file mode 100644 index 6174abe..0000000 --- a/src/ghidra/scripts/symbols.py +++ /dev/null @@ -1,162 +0,0 @@ -# Symbol operations script -# @category CLI - -import sys -import json - -def list_symbols(name_filter): - """List all symbols in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - symbol_table = currentProgram.getSymbolTable() - symbols = [] - - for symbol in symbol_table.getAllSymbols(True): - name = symbol.getName() - - if name_filter and name_filter.lower() not in name.lower(): - continue - - symbol_data = { - "name": name, - "address": str(symbol.getAddress()), - "type": str(symbol.getSymbolType()), - "source": str(symbol.getSource()), - "is_primary": symbol.isPrimary() - } - symbols.append(symbol_data) - - return {"symbols": symbols, "count": len(symbols)} - -def get_symbol(address_or_name): - """Get symbol at specific address or by name.""" - if currentProgram is None: - return {"error": "No program loaded"} - - symbol_table = currentProgram.getSymbolTable() - - if address_or_name.startswith("0x") or all(c in "0123456789abcdefABCDEF" for c in address_or_name): - try: - addr = currentProgram.getAddressFactory().getAddress(address_or_name) - if addr is None: - return {"error": "Invalid address: " + address_or_name} - - symbols_at_addr = symbol_table.getSymbols(addr) - if not symbols_at_addr: - return {"error": "No symbol at address: " + address_or_name} - - result_symbols = [] - for symbol in symbols_at_addr: - result_symbols.append({ - "name": symbol.getName(), - "address": str(symbol.getAddress()), - "type": str(symbol.getSymbolType()), - "source": str(symbol.getSource()) - }) - return {"symbols": result_symbols} - except Exception as e: - return {"error": "Failed to get symbol: " + str(e)} - else: - symbols = list(symbol_table.getSymbols(address_or_name)) - if not symbols: - return {"error": "Symbol not found: " + address_or_name} - - result_symbols = [] - for symbol in symbols: - result_symbols.append({ - "name": symbol.getName(), - "address": str(symbol.getAddress()), - "type": str(symbol.getSymbolType()), - "source": str(symbol.getSource()) - }) - return {"symbols": result_symbols} - -def create_symbol(address_str, name): - """Create a new symbol.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - symbol_table = currentProgram.getSymbolTable() - from ghidra.program.model.symbol import SourceType - - symbol_table.createLabel(addr, name, SourceType.USER_DEFINED) - - return {"status": "created", "address": address_str, "name": name} - except Exception as e: - return {"error": "Failed to create symbol: " + str(e)} - -def delete_symbol(name): - """Delete a symbol by name.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - symbol_table = currentProgram.getSymbolTable() - symbols = list(symbol_table.getSymbols(name)) - - if not symbols: - return {"error": "Symbol not found: " + name} - - for symbol in symbols: - symbol.delete() - - return {"status": "deleted", "name": name} - except Exception as e: - return {"error": "Failed to delete symbol: " + str(e)} - -def rename_symbol(old_name, new_name): - """Rename a symbol.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - symbol_table = currentProgram.getSymbolTable() - symbols = list(symbol_table.getSymbols(old_name)) - - if not symbols: - return {"error": "Symbol not found: " + old_name} - - from ghidra.program.model.symbol import SourceType - for symbol in symbols: - symbol.setName(new_name, SourceType.USER_DEFINED) - - return {"status": "renamed", "old_name": old_name, "new_name": new_name} - except Exception as e: - return {"error": "Failed to rename symbol: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "list": - result = list_symbols(args[1] if len(args) > 1 else None) - elif command == "get": - result = get_symbol(args[1] if len(args) > 1 else None) - elif command == "create": - result = create_symbol(args[1] if len(args) > 1 else None, args[2] if len(args) > 2 else None) - elif command == "delete": - result = delete_symbol(args[1] if len(args) > 1 else None) - elif command == "rename": - result = rename_symbol(args[1] if len(args) > 1 else None, args[2] if len(args) > 2 else None) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/scripts/types.py b/src/ghidra/scripts/types.py deleted file mode 100644 index 302bb9a..0000000 --- a/src/ghidra/scripts/types.py +++ /dev/null @@ -1,137 +0,0 @@ -# Type operations script -# @category CLI - -import sys -import json - -def list_types(): - """List all defined types in the program.""" - if currentProgram is None: - return {"error": "No program loaded"} - - data_type_manager = currentProgram.getDataTypeManager() - types = [] - - for data_type in data_type_manager.getAllDataTypes(): - type_data = { - "name": data_type.getName(), - "path": data_type.getPathName(), - "category": data_type.getCategoryPath().toString(), - "size": data_type.getLength() - } - types.append(type_data) - - return {"types": types, "count": len(types)} - -def get_type(type_name): - """Get type definition by name.""" - if currentProgram is None: - return {"error": "No program loaded"} - - data_type_manager = currentProgram.getDataTypeManager() - - data_type = data_type_manager.getDataType(type_name) - if data_type is None: - for dt in data_type_manager.getAllDataTypes(): - if dt.getName() == type_name: - data_type = dt - break - - if data_type is None: - return {"error": "Type not found: " + type_name} - - type_info = { - "name": data_type.getName(), - "path": data_type.getPathName(), - "category": data_type.getCategoryPath().toString(), - "size": data_type.getLength(), - "description": data_type.getDescription() - } - - from ghidra.program.model.data import Structure, Union - if isinstance(data_type, Structure) or isinstance(data_type, Union): - components = [] - for component in data_type.getComponents(): - components.append({ - "name": component.getFieldName(), - "type": component.getDataType().getName(), - "offset": component.getOffset(), - "size": component.getLength() - }) - type_info["components"] = components - - return type_info - -def create_type(type_name): - """Create a new empty struct type.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - from ghidra.program.model.data import StructureDataType - data_type_manager = currentProgram.getDataTypeManager() - - new_struct = StructureDataType(type_name, 0) - data_type_manager.addDataType(new_struct, None) - - return {"status": "created", "name": type_name} - except Exception as e: - return {"error": "Failed to create type: " + str(e)} - -def apply_type(address_str, type_name): - """Apply a type to a specific address.""" - if currentProgram is None: - return {"error": "No program loaded"} - - try: - addr = currentProgram.getAddressFactory().getAddress(address_str) - if addr is None: - return {"error": "Invalid address: " + address_str} - - data_type_manager = currentProgram.getDataTypeManager() - data_type = data_type_manager.getDataType(type_name) - - if data_type is None: - for dt in data_type_manager.getAllDataTypes(): - if dt.getName() == type_name: - data_type = dt - break - - if data_type is None: - return {"error": "Type not found: " + type_name} - - listing = currentProgram.getListing() - listing.createData(addr, data_type) - - return {"status": "applied", "address": address_str, "type": type_name} - except Exception as e: - return {"error": "Failed to apply type: " + str(e)} - -if __name__ == "__main__": - try: - if len(args) < 1: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": "No command specified"})) - print("---GHIDRA_CLI_END---") - sys.exit(1) - - command = args[0] - - if command == "list": - result = list_types() - elif command == "get": - result = get_type(args[1] if len(args) > 1 else None) - elif command == "create": - result = create_type(args[1] if len(args) > 1 else None) - elif command == "apply": - result = apply_type(args[1] if len(args) > 1 else None, args[2] if len(args) > 2 else None) - else: - result = {"error": "Unknown command: " + command} - - print("---GHIDRA_CLI_START---") - print(json.dumps(result)) - print("---GHIDRA_CLI_END---") - except Exception as e: - print("---GHIDRA_CLI_START---") - print(json.dumps({"error": str(e)})) - print("---GHIDRA_CLI_END---") diff --git a/src/ghidra/setup.rs b/src/ghidra/setup.rs index 8ab4227..25e2cec 100644 --- a/src/ghidra/setup.rs +++ b/src/ghidra/setup.rs @@ -239,111 +239,6 @@ pub async fn install_ghidra(version: Option, target_dir: PathBuf) -> Res Ok(install_path) } -/// Install PyGhidra into a venv for the given Ghidra installation. -/// This is required for Python scripting support in Ghidra 12+. -pub fn install_pyghidra(ghidra_install_dir: &Path) -> Result<()> { - use std::process::Command; - - println!("\nSetting up PyGhidra (Python scripting support)..."); - - // Find the PyGhidra wheel in the Ghidra distribution - let dist_dir = ghidra_install_dir - .join("Ghidra") - .join("Features") - .join("PyGhidra") - .join("pypkg") - .join("dist"); - - if !dist_dir.exists() { - println!("⚠ PyGhidra dist directory not found - skipping PyGhidra setup"); - println!(" (This Ghidra version may not include PyGhidra)"); - return Ok(()); - } - - // Find the wheel file - let wheel_path = std::fs::read_dir(&dist_dir)? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .find(|p| { - p.extension().map(|e| e == "whl").unwrap_or(false) - && p.file_name() - .map(|n| n.to_string_lossy().starts_with("pyghidra")) - .unwrap_or(false) - }) - .ok_or_else(|| anyhow!("PyGhidra wheel not found in {}", dist_dir.display()))?; - - println!( - " Found PyGhidra wheel: {}", - wheel_path.file_name().unwrap_or_default().to_string_lossy() - ); - - // Determine venv location (matches pyghidra_launcher.py logic) - // Format: ~/.config/ghidra/ghidra__/venv - let ghidra_dir_name = ghidra_install_dir - .file_name() - .ok_or_else(|| anyhow!("Invalid Ghidra install path"))? - .to_string_lossy(); - - let venv_dir = dirs::config_dir() - .ok_or_else(|| anyhow!("Could not determine config directory"))? - .join("ghidra") - .join(ghidra_dir_name.as_ref()) - .join("venv"); - - // Create venv if it doesn't exist - if !venv_dir.exists() { - println!(" Creating Python virtual environment..."); - let status = Command::new("python3") - .args(["-m", "venv"]) - .arg(&venv_dir) - .status() - .context("Failed to create Python venv")?; - - if !status.success() { - return Err(anyhow!("Failed to create Python virtual environment")); - } - } - - // Get pip path in venv - #[cfg(unix)] - let pip_path = venv_dir.join("bin").join("pip"); - #[cfg(windows)] - let pip_path = venv_dir.join("Scripts").join("pip.exe"); - - // Install PyGhidra - println!(" Installing PyGhidra..."); - let status = Command::new(&pip_path) - .args(["install", "--no-index", "-f"]) - .arg(&dist_dir) - .arg("pyghidra") - .status() - .context("Failed to run pip install")?; - - if !status.success() { - return Err(anyhow!("Failed to install PyGhidra")); - } - - // Verify installation - #[cfg(unix)] - let python_path = venv_dir.join("bin").join("python3"); - #[cfg(windows)] - let python_path = venv_dir.join("Scripts").join("python.exe"); - - let output = Command::new(&python_path) - .args(["-c", "import pyghidra; print(pyghidra.__version__)"]) - .output() - .context("Failed to verify PyGhidra installation")?; - - if output.status.success() { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - println!("✓ PyGhidra {} installed successfully", version); - } else { - println!("⚠ PyGhidra installed but verification failed"); - } - - Ok(()) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 12a9a2d..84333f3 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -35,8 +35,9 @@ impl BridgeClient { command: &str, args: Option, ) -> Result { - let mut stream = TcpStream::connect(format!("127.0.0.1:{}", self.port)) - .map_err(|e| anyhow::anyhow!("Failed to connect to bridge on port {}: {}", self.port, e))?; + let mut stream = TcpStream::connect(format!("127.0.0.1:{}", self.port)).map_err(|e| { + anyhow::anyhow!("Failed to connect to bridge on port {}: {}", self.port, e) + })?; stream.set_read_timeout(Some(Duration::from_secs(300))).ok(); stream.set_write_timeout(Some(Duration::from_secs(30))).ok(); @@ -62,7 +63,9 @@ impl BridgeClient { match response.status.as_str() { "success" => Ok(response.data.unwrap_or(json!({}))), "error" => { - let msg = response.message.unwrap_or_else(|| "Unknown error".to_string()); + let msg = response + .message + .unwrap_or_else(|| "Unknown error".to_string()); anyhow::bail!("{}", msg) } "shutdown" => Ok(json!({"status": "shutdown"})), @@ -179,7 +182,10 @@ impl BridgeClient { } pub fn symbol_create(&self, address: &str, name: &str) -> Result { - self.send_command("symbol_create", Some(json!({"address": address, "name": name}))) + self.send_command( + "symbol_create", + Some(json!({"address": address, "name": name})), + ) } pub fn symbol_delete(&self, name: &str) -> Result { @@ -187,7 +193,10 @@ impl BridgeClient { } pub fn symbol_rename(&self, old_name: &str, new_name: &str) -> Result { - self.send_command("symbol_rename", Some(json!({"old_name": old_name, "new_name": new_name}))) + self.send_command( + "symbol_rename", + Some(json!({"old_name": old_name, "new_name": new_name})), + ) } pub fn type_list(&self) -> Result { @@ -203,7 +212,10 @@ impl BridgeClient { } pub fn type_apply(&self, address: &str, type_name: &str) -> Result { - self.send_command("type_apply", Some(json!({"address": address, "type_name": type_name}))) + self.send_command( + "type_apply", + Some(json!({"address": address, "type_name": type_name})), + ) } pub fn comment_list(&self) -> Result { @@ -214,12 +226,20 @@ impl BridgeClient { self.send_command("comment_get", Some(json!({"address": address}))) } - pub fn comment_set(&self, address: &str, text: &str, comment_type: Option<&str>) -> Result { - self.send_command("comment_set", Some(json!({ - "address": address, - "text": text, - "type": comment_type, - }))) + pub fn comment_set( + &self, + address: &str, + text: &str, + comment_type: Option<&str>, + ) -> Result { + self.send_command( + "comment_set", + Some(json!({ + "address": address, + "text": text, + "type": comment_type, + })), + ) } pub fn comment_delete(&self, address: &str) -> Result { @@ -231,11 +251,17 @@ impl BridgeClient { } pub fn graph_callers(&self, function: &str, depth: Option) -> Result { - self.send_command("graph_callers", Some(json!({"function": function, "depth": depth}))) + self.send_command( + "graph_callers", + Some(json!({"function": function, "depth": depth})), + ) } pub fn graph_callees(&self, function: &str, depth: Option) -> Result { - self.send_command("graph_callees", Some(json!({"function": function, "depth": depth}))) + self.send_command( + "graph_callees", + Some(json!({"function": function, "depth": depth})), + ) } pub fn graph_export(&self, format: &str) -> Result { @@ -267,11 +293,17 @@ impl BridgeClient { } pub fn diff_programs(&self, program1: &str, program2: &str) -> Result { - self.send_command("diff_programs", Some(json!({"program1": program1, "program2": program2}))) + self.send_command( + "diff_programs", + Some(json!({"program1": program1, "program2": program2})), + ) } pub fn diff_functions(&self, func1: &str, func2: &str) -> Result { - self.send_command("diff_functions", Some(json!({"func1": func1, "func2": func2}))) + self.send_command( + "diff_functions", + Some(json!({"func1": func1, "func2": func2})), + ) } pub fn patch_bytes(&self, address: &str, hex: &str) -> Result { @@ -286,8 +318,15 @@ impl BridgeClient { self.send_command("patch_export", Some(json!({"output": output}))) } - pub fn disasm(&self, address: &str, num_instructions: Option) -> Result { - self.send_command("disasm", Some(json!({"address": address, "count": num_instructions}))) + pub fn disasm( + &self, + address: &str, + num_instructions: Option, + ) -> Result { + self.send_command( + "disasm", + Some(json!({"address": address, "count": num_instructions})), + ) } pub fn stats(&self) -> Result { @@ -295,7 +334,10 @@ impl BridgeClient { } pub fn script_run(&self, script_path: &str, args: &[String]) -> Result { - self.send_command("script_run", Some(json!({"path": script_path, "args": args}))) + self.send_command( + "script_run", + Some(json!({"path": script_path, "args": args})), + ) } pub fn script_python(&self, code: &str) -> Result { @@ -323,6 +365,9 @@ impl BridgeClient { } pub fn program_export(&self, format: &str, output: Option<&str>) -> Result { - self.send_command("export_program", Some(json!({"format": format, "output": output}))) + self.send_command( + "export_program", + Some(json!({"format": format, "output": output})), + ) } } diff --git a/src/main.rs b/src/main.rs index 052b32e..9f38d79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,1067 +1,1056 @@ -mod cli; -mod config; -mod daemon; -mod error; -mod filter; -mod format; -mod ghidra; -mod ipc; -mod query; - -use clap::Parser; -use cli::{Cli, Commands, DaemonCommands}; -use config::Config; -use error::{GhidraError, Result}; -use format::{auto_detect_format, DefaultFormatter, Formatter, OutputFormat}; -use ghidra::bridge::{self, BridgeStartMode, BridgeStatus}; -use ghidra::GhidraClient; -use ipc::client::BridgeClient; -use std::path::{Path, PathBuf}; - - - -fn main() { - // Initialize logging with info level by default, can be overridden via RUST_LOG - let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); - tracing_subscriber::fmt() - .with_env_filter(env_filter) - .init(); - - let cli = Cli::parse(); - - let result = match &cli.command { - Commands::Setup(_) => { - // Setup needs async for downloading - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(run_setup(cli)) - } - Commands::Daemon(_) => handle_daemon_command_dispatch(cli), - _ => run_command(cli), - }; - - if let Err(e) = result { - eprintln!("Error: {}", e); - std::process::exit(1); - } -} - -/// Run a command, starting the bridge if needed. -fn run_command(cli: Cli) -> anyhow::Result<()> { - match &cli.command { - // Non-bridge commands - Commands::Init => handle_init(), - Commands::Doctor => handle_doctor(), - Commands::Version => handle_version(), - Commands::Config(cmd) => handle_config_command(cmd.clone()), - Commands::SetDefault(args) => handle_set_default(args.clone()), - Commands::Project(args) => handle_project_command(args.command.clone()), - // Commands requiring bridge - _ if requires_bridge(&cli.command) => { - run_with_bridge(cli) - } - _ => { - println!("Command not yet implemented"); - Ok(()) - } - } -} - -/// Determines if a command requires the bridge to be running. -fn requires_bridge(command: &Commands) -> bool { - matches!( - command, - Commands::Import(_) - | Commands::Analyze(_) - | Commands::Quick(_) - | Commands::Query(_) - | Commands::Decompile(_) - | Commands::Function(_) - | Commands::Strings(_) - | Commands::Memory(_) - | Commands::Dump(_) - | Commands::Summary(_) - | Commands::XRef(_) - | Commands::Symbol(_) - | Commands::Type(_) - | Commands::Comment(_) - | Commands::Graph(_) - | Commands::Find(_) - | Commands::Diff(_) - | Commands::Patch(_) - | Commands::Script(_) - | Commands::Disasm(_) - | Commands::Batch(_) - | Commands::Stats(_) - | Commands::Program(_) - ) -} - -/// Extract the project name from a command's args (if present). -fn extract_project_from_command(command: &Commands) -> Option { - match command { - Commands::Import(args) => args.project.clone(), - Commands::Analyze(args) => args.project.clone(), - Commands::Quick(args) => args.project.clone(), - Commands::Query(args) => args.project.clone(), - Commands::Summary(args) => args.options.project.clone(), - Commands::Decompile(args) => args.options.project.clone(), - Commands::Function(cmd) => match cmd { - cli::FunctionCommands::List(opts) => opts.project.clone(), - cli::FunctionCommands::Decompile(args) => args.options.project.clone(), - cli::FunctionCommands::Get(args) => args.options.project.clone(), - cli::FunctionCommands::Disasm(args) => args.options.project.clone(), - cli::FunctionCommands::Calls(args) => args.options.project.clone(), - cli::FunctionCommands::XRefs(args) => args.options.project.clone(), - cli::FunctionCommands::Rename(args) => args.project.clone(), - cli::FunctionCommands::Create(args) => args.project.clone(), - cli::FunctionCommands::Delete(args) => args.options.project.clone(), - }, - Commands::Strings(cmd) => match cmd { - cli::StringsCommands::List(opts) => opts.project.clone(), - cli::StringsCommands::Refs(args) => args.options.project.clone(), - }, - Commands::Memory(cmd) => match cmd { - cli::MemoryCommands::Map(opts) => opts.project.clone(), - cli::MemoryCommands::Read(args) => args.options.project.clone(), - cli::MemoryCommands::Write(args) => args.project.clone(), - cli::MemoryCommands::Search(args) => args.options.project.clone(), - }, - Commands::Dump(cmd) => match cmd { - cli::DumpCommands::Imports(opts) => opts.project.clone(), - cli::DumpCommands::Exports(opts) => opts.project.clone(), - cli::DumpCommands::Functions(opts) => opts.project.clone(), - cli::DumpCommands::Strings(opts) => opts.project.clone(), - }, - Commands::XRef(cmd) => match cmd { - cli::XRefCommands::To(args) => args.options.project.clone(), - cli::XRefCommands::From(args) => args.options.project.clone(), - cli::XRefCommands::List(args) => args.options.project.clone(), - }, - Commands::Stats(args) => args.options.project.clone(), - Commands::Disasm(args) => args.options.project.clone(), - Commands::Find(cmd) => match cmd { - cli::FindCommands::String(args) => args.options.project.clone(), - cli::FindCommands::Bytes(args) => args.options.project.clone(), - cli::FindCommands::Function(args) => args.options.project.clone(), - cli::FindCommands::Calls(args) => args.options.project.clone(), - cli::FindCommands::Crypto(opts) => opts.project.clone(), - cli::FindCommands::Interesting(opts) => opts.project.clone(), - }, - Commands::Graph(cmd) => match cmd { - cli::GraphCommands::Calls(opts) => opts.project.clone(), - cli::GraphCommands::Callers(args) => args.options.project.clone(), - cli::GraphCommands::Callees(args) => args.options.project.clone(), - cli::GraphCommands::Export(args) => args.options.project.clone(), - }, - Commands::Comment(cmd) => match cmd { - cli::CommentCommands::List(opts) => opts.project.clone(), - cli::CommentCommands::Get(args) => args.options.project.clone(), - cli::CommentCommands::Set(args) => args.project.clone(), - cli::CommentCommands::Delete(args) => args.options.project.clone(), - }, - Commands::Symbol(cmd) => match cmd { - cli::SymbolCommands::List(opts) => opts.project.clone(), - cli::SymbolCommands::Get(args) => args.options.project.clone(), - cli::SymbolCommands::Create(args) => args.project.clone(), - cli::SymbolCommands::Delete(args) => args.options.project.clone(), - cli::SymbolCommands::Rename(args) => args.project.clone(), - }, - Commands::Type(cmd) => match cmd { - cli::TypeCommands::List(opts) => opts.project.clone(), - cli::TypeCommands::Get(args) => args.options.project.clone(), - cli::TypeCommands::Create(args) => args.project.clone(), - cli::TypeCommands::Apply(args) => args.project.clone(), - }, - Commands::Patch(cmd) => match cmd { - cli::PatchCommands::Bytes(args) => args.project.clone(), - cli::PatchCommands::Nop(args) => args.project.clone(), - cli::PatchCommands::Export(args) => args.project.clone(), - }, - Commands::Script(cmd) => match cmd { - cli::ScriptCommands::Run(args) => args.project.clone(), - cli::ScriptCommands::Python(args) => args.project.clone(), - cli::ScriptCommands::Java(args) => args.project.clone(), - cli::ScriptCommands::List => None, - }, - Commands::Program(cmd) => match cmd { - cli::ProgramCommands::List(args) => args.project.clone(), - cli::ProgramCommands::Open(args) => args.project.clone(), - cli::ProgramCommands::Close(args) => args.project.clone(), - cli::ProgramCommands::Delete(args) => args.project.clone(), - cli::ProgramCommands::Info(args) => args.project.clone(), - cli::ProgramCommands::Export(args) => args.project.clone(), - }, - _ => None, - } -} - -/// Run a command that requires the bridge. -fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { - let config = Config::load()?; - - // Extract project from command args, fall back to config default - let project_from_cmd = extract_project_from_command(&cli.command); - let project_path = resolve_project_path(&project_from_cmd, &config)?; - - let ghidra_install_dir = config - .ghidra_install_dir - .clone() - .or_else(|| config.get_ghidra_install_dir().ok()) - .ok_or_else(|| anyhow::anyhow!( - "Ghidra installation directory not configured. Run 'ghidra setup' first." - ))?; - - // For Import and Quick, we may need to start a new bridge - let client = match &cli.command { - Commands::Import(args) => { - let binary_path = PathBuf::from(&args.binary); - if !binary_path.exists() { - anyhow::bail!("Binary not found: {}", args.binary); - } - - // Check if bridge is already running - if bridge::is_bridge_running(&project_path) { - // Bridge running - import via bridge command - let port = bridge::read_port_file(&project_path)? - .ok_or_else(|| anyhow::anyhow!("Bridge port file not found"))?; - let client = BridgeClient::new(port); - let result = client.import_binary(&args.binary, args.program.as_deref())?; - - let program_name = args.program.clone().unwrap_or_else(|| { - result.get("program") - .and_then(|p| p.as_str()) - .unwrap_or("unknown") - .to_string() - }); - - // Switch to the newly imported program - client.open_program(&program_name)?; - println!("Successfully imported as: {}", program_name); - return Ok(()); - } - - // No bridge running - start one in import mode - eprintln!("Starting Ghidra bridge..."); - let port = bridge::ensure_bridge_running( - &project_path, - &ghidra_install_dir, - BridgeStartMode::Import { - binary_path: args.binary.clone(), - }, - )?; - let client = BridgeClient::new(port); - let info = client.program_info()?; - let program_name = args.program.clone().unwrap_or_else(|| { - info.get("name") - .and_then(|n| n.as_str()) - .unwrap_or("unknown") - .to_string() - }); - println!("Successfully imported as: {}", program_name); - return Ok(()); - } - - Commands::Quick(args) => { - let binary_path = PathBuf::from(&args.binary); - if !binary_path.exists() { - anyhow::bail!("Binary not found: {}", args.binary); - } - - println!("Quick analysis of {}...\n", args.binary); - - println!("[1/3] Importing binary..."); - let port = bridge::ensure_bridge_running( - &project_path, - &ghidra_install_dir, - BridgeStartMode::Import { - binary_path: args.binary.clone(), - }, - )?; - let client = BridgeClient::new(port); - - client.program_info()?; - - println!("[2/3] Running analysis..."); - client.analyze()?; - - println!("[3/3] Done!\n"); - println!("Analysis complete. The bridge is running on port {}.", port); - println!("\nRun queries like:"); - println!(" ghidra function list"); - println!(" ghidra decompile main"); - println!(" ghidra summary"); - - return Ok(()); - } - - Commands::Analyze(args) => { - let program = resolve_program(&args.program, &config)?; - - // If bridge is already running, just send analyze command - if bridge::is_bridge_running(&project_path) { - let client = connect_to_bridge(&project_path)?; - println!("Analyzing {}...", program); - client.analyze()?; - println!("Analysis complete!"); - return Ok(()); - } - - // Start bridge in process mode - eprintln!("Starting Ghidra bridge..."); - let port = bridge::ensure_bridge_running( - &project_path, - &ghidra_install_dir, - BridgeStartMode::Process { - program_name: program.clone(), - }, - )?; - let client = BridgeClient::new(port); - println!("Analyzing {}...", program); - client.analyze()?; - println!("Analysis complete!"); - return Ok(()); - } - - _ => { - // For query commands, ensure bridge is running (auto-start in process mode if needed) - if !bridge::is_bridge_running(&project_path) { - // Need a program name to start the bridge in process mode - let program = config.get_default_program() - .ok_or_else(|| anyhow::anyhow!( - "No bridge running and no default program configured.\n\ - Import a binary first: ghidra import \n\ - Or set a default: ghidra set-default program " - ))?; - - eprintln!("Starting Ghidra bridge..."); - let port = bridge::ensure_bridge_running( - &project_path, - &ghidra_install_dir, - BridgeStartMode::Process { - program_name: program, - }, - )?; - eprintln!("Bridge ready."); - BridgeClient::new(port) - } else { - connect_to_bridge(&project_path)? - } - } - }; - - // Execute the command via bridge - let result = execute_via_bridge(&client, &cli.command)?; - - // Determine output format based on flags and TTY detection - let format = if cli.pretty { - OutputFormat::Json - } else if cli.json { - OutputFormat::JsonCompact - } else { - auto_detect_format(atty::is(atty::Stream::Stdout)) - }; - - // Detect if result is already an array before wrapping - let values = match result { - serde_json::Value::Array(arr) => arr, - single => vec![single], - }; - - let formatter = DefaultFormatter; - let output = formatter.format(&values, format)?; - if !output.is_empty() { - println!("{}", output); - } - Ok(()) -} - -/// Execute a command via the bridge client. -fn execute_via_bridge( - client: &BridgeClient, - command: &Commands, -) -> anyhow::Result { - use serde_json::json; - - match command { - Commands::Query(args) => match args.data_type.as_str() { - "functions" => client.list_functions(args.limit, args.filter.clone()), - "strings" => client.list_strings(args.limit), - "imports" => client.list_imports(), - "exports" => client.list_exports(), - "memory" => client.memory_map(), - other => anyhow::bail!("Query type '{}' not supported", other), - }, - Commands::Decompile(args) => client.decompile(args.target.clone()), - Commands::Function(cmd) => { - use cli::FunctionCommands; - match cmd { - FunctionCommands::List(opts) => { - client.list_functions(opts.limit, opts.filter.clone()) - } - FunctionCommands::Decompile(args) => client.decompile(args.target.clone()), - FunctionCommands::Get(args) => { - client.send_command("get_function", Some(json!({"address": args.target}))) - } - FunctionCommands::Disasm(args) => { - client.disasm(&args.target, None) - } - FunctionCommands::Calls(args) => { - client.find_calls(&args.target) - } - FunctionCommands::XRefs(args) => { - client.xrefs_to(args.target.clone()) - } - FunctionCommands::Rename(args) => { - client.send_command("rename_function", Some(json!({ - "old_name": args.old_name, - "new_name": args.new_name, - }))) - } - FunctionCommands::Create(args) => { - client.send_command("create_function", Some(json!({ - "address": args.address, - "name": args.name, - }))) - } - FunctionCommands::Delete(args) => { - client.send_command("delete_function", Some(json!({ - "address": args.target, - }))) - } - } - } - Commands::Strings(cmd) => { - use cli::StringsCommands; - match cmd { - StringsCommands::List(opts) => client.list_strings(opts.limit), - StringsCommands::Refs(args) => { - client.xrefs_to(args.string.clone()) - } - } - } - Commands::Memory(cmd) => { - use cli::MemoryCommands; - match cmd { - MemoryCommands::Map(_) => client.memory_map(), - MemoryCommands::Read(args) => { - client.send_command("read_memory", Some(json!({ - "address": args.address, - "size": args.size, - }))) - } - MemoryCommands::Write(args) => { - client.send_command("write_memory", Some(json!({ - "address": args.address, - "bytes": args.bytes, - }))) - } - MemoryCommands::Search(args) => { - client.send_command("search_memory", Some(json!({ - "pattern": args.pattern, - }))) - } - } - } - Commands::Dump(cmd) => { - use cli::DumpCommands; - match cmd { - DumpCommands::Imports(_) => client.list_imports(), - DumpCommands::Exports(_) => client.list_exports(), - DumpCommands::Functions(opts) => { - client.list_functions(opts.limit, opts.filter.clone()) - } - DumpCommands::Strings(opts) => client.list_strings(opts.limit), - } - } - Commands::Summary(_) => client.program_info(), - Commands::XRef(cmd) => { - use cli::XRefCommands; - match cmd { - XRefCommands::To(args) => client.xrefs_to(args.address.clone()), - XRefCommands::From(args) => client.xrefs_from(args.address.clone()), - XRefCommands::List(_) => { - client.send_command("xrefs_list", None) - } - } - } - Commands::Program(cmd) => { - use cli::ProgramCommands; - match cmd { - ProgramCommands::List(_) => client.list_programs(), - ProgramCommands::Open(args) => { - let program = args.program.as_ref() - .ok_or_else(|| anyhow::anyhow!("Program name required. Use --program "))?; - client.open_program(program) - } - ProgramCommands::Close(_) => client.program_close(), - ProgramCommands::Delete(args) => { - let program = args.program.as_ref() - .ok_or_else(|| anyhow::anyhow!("Program name required"))?; - client.program_delete(program) - } - ProgramCommands::Info(_) => client.program_info(), - ProgramCommands::Export(args) => { - client.program_export(&args.format, args.output.as_deref()) - } - } - } - Commands::Symbol(cmd) => { - use cli::SymbolCommands; - match cmd { - SymbolCommands::List(opts) => client.symbol_list(opts.filter.as_deref()), - SymbolCommands::Get(args) => client.symbol_get(&args.name), - SymbolCommands::Create(args) => client.symbol_create(&args.address, &args.name), - SymbolCommands::Delete(args) => client.symbol_delete(&args.name), - SymbolCommands::Rename(args) => client.symbol_rename(&args.old_name, &args.new_name), - } - } - Commands::Type(cmd) => { - use cli::TypeCommands; - match cmd { - TypeCommands::List(_) => client.type_list(), - TypeCommands::Get(args) => client.type_get(&args.name), - TypeCommands::Create(args) => client.type_create(&args.definition), - TypeCommands::Apply(args) => client.type_apply(&args.address, &args.type_name), - } - } - Commands::Comment(cmd) => { - use cli::CommentCommands; - match cmd { - CommentCommands::List(_) => client.comment_list(), - CommentCommands::Get(args) => client.comment_get(&args.address), - CommentCommands::Set(args) => { - client.comment_set(&args.address, &args.text, args.comment_type.as_deref()) - } - CommentCommands::Delete(args) => client.comment_delete(&args.address), - } - } - Commands::Graph(cmd) => { - use cli::GraphCommands; - match cmd { - GraphCommands::Calls(opts) => client.graph_calls(opts.limit), - GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth), - GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth), - GraphCommands::Export(args) => client.graph_export(&args.format), - } - } - Commands::Find(cmd) => { - use cli::FindCommands; - match cmd { - FindCommands::String(args) => client.find_string(&args.pattern), - FindCommands::Bytes(args) => client.find_bytes(&args.hex), - FindCommands::Function(args) => client.find_function(&args.pattern), - FindCommands::Calls(args) => client.find_calls(&args.function), - FindCommands::Crypto(_) => client.find_crypto(), - FindCommands::Interesting(_) => client.find_interesting(), - } - } - Commands::Diff(cmd) => { - use cli::DiffCommands; - match cmd { - DiffCommands::Programs(args) => client.diff_programs(&args.program1, &args.program2), - DiffCommands::Functions(args) => client.diff_functions(&args.func1, &args.func2), - } - } - Commands::Patch(cmd) => { - use cli::PatchCommands; - match cmd { - PatchCommands::Bytes(args) => client.patch_bytes(&args.address, &args.hex), - PatchCommands::Nop(args) => client.patch_nop(&args.address), - PatchCommands::Export(args) => client.patch_export(&args.output), - } - } - Commands::Script(cmd) => { - use cli::ScriptCommands; - match cmd { - ScriptCommands::Run(args) => client.script_run(&args.script_path, &args.args), - ScriptCommands::Python(args) => client.script_python(&args.code), - ScriptCommands::Java(args) => client.script_java(&args.code), - ScriptCommands::List => client.script_list(), - } - } - Commands::Disasm(args) => { - client.disasm(&args.address, args.num_instructions) - } - Commands::Batch(args) => { - // Read batch file and send commands - let content = std::fs::read_to_string(&args.script_file) - .map_err(|e| anyhow::anyhow!("Failed to read batch file: {}", e))?; - let commands: Vec = content - .lines() - .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#')) - .map(|l| { - serde_json::from_str(l).unwrap_or_else(|_| { - json!({"command": l.trim()}) - }) - }) - .collect(); - client.batch(&commands) - } - Commands::Stats(_) => client.stats(), - _ => anyhow::bail!("Command not supported"), - } -} - -/// Dispatch daemon (bridge management) commands. -fn handle_daemon_command_dispatch(cli: Cli) -> anyhow::Result<()> { - match cli.command { - Commands::Daemon(cmd) => match cmd { - DaemonCommands::Start { - project, - program, - port: _, - foreground: _, - } => handle_bridge_start(project, program), - DaemonCommands::Stop { project } => handle_bridge_stop(project), - DaemonCommands::Restart { - project, - program, - port: _, - } => { - handle_bridge_stop(project.clone())?; - std::thread::sleep(std::time::Duration::from_secs(1)); - handle_bridge_start(project, program) - } - DaemonCommands::Status { project } => handle_bridge_status(project), - DaemonCommands::Ping { project } => handle_bridge_ping(project), - DaemonCommands::ClearCache { project: _ } => { - println!("Cache is managed by the bridge process"); - Ok(()) - } - }, - _ => unreachable!(), - } -} - -/// Start the bridge for a project. -fn handle_bridge_start( - project: Option, - program: Option, -) -> anyhow::Result<()> { - let config = Config::load()?; - let project_path = resolve_project_path(&project, &config)?; - - let ghidra_install_dir = config - .ghidra_install_dir - .clone() - .or_else(|| config.get_ghidra_install_dir().ok()) - .ok_or_else(|| anyhow::anyhow!( - "Ghidra installation directory not configured. Run 'ghidra setup' first." - ))?; - - // Check if bridge is already running - if bridge::is_bridge_running(&project_path) { - println!("Bridge is already running for project: {}", project_path.display()); - return Ok(()); - } - - // Determine start mode - let mode = if let Some(prog) = program { - BridgeStartMode::Process { - program_name: prog, - } - } else { - // Need a program name - let prog = config.get_default_program() - .ok_or_else(|| anyhow::anyhow!( - "No program specified. Use --program or set a default." - ))?; - BridgeStartMode::Process { - program_name: prog, - } - }; - - println!("Starting bridge for project: {}", project_path.display()); - - let port = bridge::ensure_bridge_running( - &project_path, - &ghidra_install_dir, - mode, - )?; - - println!("Bridge started on port {}", port); - Ok(()) -} - -/// Stop the bridge for a project. -fn handle_bridge_stop(project: Option) -> anyhow::Result<()> { - let config = Config::load()?; - let project_path = resolve_project_path(&project, &config)?; - - if bridge::is_bridge_running(&project_path) { - println!("Stopping bridge..."); - bridge::stop_bridge(&project_path)?; - println!("Bridge stopped"); - } else { - println!("No bridge running for project: {}", project_path.display()); - } - - Ok(()) -} - -/// Get bridge status for a project. -fn handle_bridge_status(project: Option) -> anyhow::Result<()> { - let config = Config::load()?; - let project_path = resolve_project_path(&project, &config)?; - - match bridge::bridge_status(&project_path)? { - BridgeStatus::Running { port, pid } => { - println!("Bridge is running:"); - println!(" PID: {}", pid); - println!(" Port: {}", port); - println!(" Project: {}", project_path.display()); - } - BridgeStatus::Stopped => { - println!("No bridge running for project: {}", project_path.display()); - } - } - - Ok(()) -} - -/// Ping the bridge. -fn handle_bridge_ping(project: Option) -> anyhow::Result<()> { - let config = Config::load()?; - let project_path = resolve_project_path(&project, &config)?; - - if bridge::is_bridge_running(&project_path) { - let client = connect_to_bridge(&project_path)?; - if client.ping()? { - println!("Bridge is responsive"); - } else { - println!("Bridge is not responding"); - } - } else { - println!("No bridge running for project: {}", project_path.display()); - } - - Ok(()) -} - -/// Handle the setup command - download and install Ghidra. -async fn run_setup(cli: Cli) -> anyhow::Result<()> { - let args = match cli.command { - Commands::Setup(args) => args, - _ => unreachable!(), - }; - - println!("Ghidra Setup Wizard"); - println!("===================\n"); - - // 1. Check Java - if !args.force { - if let Err(e) = ghidra::setup::check_java_requirement() { - eprintln!("Java prerequisite check failed: {}", e); - eprintln!("Ghidra requires JDK 17+. Use --force to continue anyway."); - std::process::exit(1); - } - } else { - println!("Skipping Java check (--force specified)"); - } - - // 2. Determine Install Directory - let install_base = if let Some(d) = args.dir { - PathBuf::from(d) - } else { - dirs::data_local_dir() - .ok_or(anyhow::anyhow!("Could not determine data directory"))? - .join("ghidra-cli") - .join("ghidra") - }; - - std::fs::create_dir_all(&install_base)?; - - // 3. Install Ghidra - println!("\nInstalling to: {}", install_base.display()); - let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?; - - // 4. Update Config - let mut config = Config::load()?; - config.ghidra_install_dir = Some(final_path.clone()); - config.save()?; - - println!("\nSuccess! Ghidra installed at: {}", final_path.display()); - println!("Configuration updated."); - - // 5. Verify - println!("\nVerifying installation..."); - let client = GhidraClient::new(config)?; - if client.verify_installation().is_ok() { - println!("Verification passed!"); - println!("\nYou can now run: ghidra quick "); - } else { - println!("Verification failed - analyzeHeadless not found"); - println!(" The installation may be incomplete."); - } - - Ok(()) -} - -fn handle_init() -> anyhow::Result<()> { - println!("Ghidra CLI Initialization"); - println!("========================\n"); - - let mut config = Config::default(); - - if config.ghidra_install_dir.is_none() { - println!("Ghidra installation not found automatically."); - println!("Please set GHIDRA_INSTALL_DIR environment variable or run 'ghidra setup'."); - } - - // Set default project directory - let home = dirs::home_dir().ok_or_else(|| { - GhidraError::ConfigError("Could not determine home directory".to_string()) - })?; - let project_dir = home.join(".ghidra-projects"); - config.ghidra_project_dir = Some(project_dir.clone()); - - println!("\nProject directory: {}", project_dir.display()); - - // Save config - config.save()?; - - println!( - "\nConfiguration saved to: {}", - Config::config_path()?.display() - ); - println!("\nRun 'ghidra doctor' to verify your installation."); - - Ok(()) -} - -fn handle_doctor() -> anyhow::Result<()> { - println!("Ghidra CLI Doctor"); - println!("=================\n"); - - let config = Config::load()?; - - // Check Ghidra installation - print!("Checking Ghidra installation... "); - match config.get_ghidra_install_dir() { - Ok(dir) => { - println!("OK"); - println!(" Location: {}", dir.display()); - - let client = GhidraClient::new(config.clone()); - match client { - Ok(c) => { - if c.verify_installation().is_ok() { - println!(" analyzeHeadless: OK"); - } else { - println!(" analyzeHeadless: NOT FOUND"); - } - } - Err(e) => { - println!(" Error: {}", e); - } - } - } - Err(e) => { - println!("FAILED"); - println!(" Error: {}", e); - } - } - - // Check Java - print!("\nChecking Java... "); - match ghidra::setup::check_java_requirement() { - Ok(()) => println!("OK (JDK 17+)"), - Err(e) => { - println!("FAILED"); - println!(" Error: {}", e); - } - } - - // Check project directory - print!("\nChecking project directory... "); - match config.get_project_dir() { - Ok(dir) => { - println!("OK"); - println!(" Location: {}", dir.display()); - println!( - " Exists: {}", - if dir.exists() { - "yes" - } else { - "no (will be created)" - } - ); - } - Err(e) => { - println!("FAILED"); - println!(" Error: {}", e); - } - } - - // Check config file - print!("\nConfig file... "); - match Config::config_path() { - Ok(path) => { - println!("OK"); - println!(" Location: {}", path.display()); - println!(" Exists: {}", if path.exists() { "yes" } else { "no" }); - } - Err(e) => { - println!("FAILED"); - println!(" Error: {}", e); - } - } - - println!("\nDone!"); - Ok(()) -} - -fn handle_version() -> anyhow::Result<()> { - println!("ghidra-cli {}", env!("CARGO_PKG_VERSION")); - println!("Rust CLI for Ghidra reverse engineering"); - Ok(()) -} - -fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> { - use cli::ConfigCommands; - - match cmd { - ConfigCommands::List => { - let config = Config::load()?; - println!("{}", serde_yaml::to_string(&config)?); - } - ConfigCommands::Get { key } => { - let config = Config::load()?; - let yaml = serde_yaml::to_value(&config)?; - if let Some(value) = yaml.get(&key) { - println!("{}", serde_yaml::to_string(value)?); - } else { - println!("Key not found: {}", key); - } - } - ConfigCommands::Set { key, value } => { - let mut config = Config::load()?; - match key.as_str() { - "default_output_format" => config.default_output_format = Some(value), - "timeout" => { - let timeout: u64 = value.parse().map_err(|_| { - GhidraError::ConfigError("Invalid timeout value".to_string()) - })?; - config.timeout = Some(timeout); - } - _ => { - anyhow::bail!("Unknown config key: {}", key); - } - } - config.save()?; - println!("Configuration updated"); - } - ConfigCommands::Reset => { - let config = Config::default(); - config.save()?; - println!("Configuration reset to defaults"); - } - } - - Ok(()) -} - -fn handle_set_default(args: cli::SetDefaultArgs) -> anyhow::Result<()> { - let mut config = Config::load()?; - - match args.kind.as_str() { - "program" => { - config.default_program = Some(args.value.clone()); - config.save()?; - println!("Default program set to: {}", args.value); - } - "project" => { - config.default_project = Some(args.value.clone()); - config.save()?; - println!("Default project set to: {}", args.value); - } - _ => { - anyhow::bail!(format!("Unknown default kind: {}", args.kind)); - } - } - - Ok(()) -} - -fn handle_project_command(cmd: cli::ProjectCommands) -> anyhow::Result<()> { - use cli::ProjectCommands; - - let config = Config::load()?; - let client = GhidraClient::new(config)?; - - match cmd { - ProjectCommands::Create { name } => { - client.create_project(&name)?; - println!("Project '{}' created", name); - } - ProjectCommands::List => { - let project_dir = client.get_project_dir(); - if !project_dir.exists() { - println!("No projects found"); - return Ok(()); - } - - println!("Projects:"); - for entry in std::fs::read_dir(project_dir)? { - let entry = entry?; - if entry.path().is_dir() { - if let Some(name) = entry.file_name().to_str() { - println!(" {}", name); - } - } - } - } - ProjectCommands::Delete { name } => { - let project_path = client.get_project_path(&name); - if project_path.exists() { - std::fs::remove_dir_all(&project_path)?; - println!("Project '{}' deleted", name); - } else { - println!("Project '{}' not found", name); - } - } - ProjectCommands::Info { name } => { - let project_name = name.unwrap_or_else(|| "default".to_string()); - let project_path = client.get_project_path(&project_name); - println!("Project: {}", project_name); - println!("Path: {}", project_path.display()); - println!("Exists: {}", project_path.exists()); - } - } - - Ok(()) -} - -fn resolve_program(program: &Option, config: &Config) -> Result { - program - .clone() - .or_else(|| config.get_default_program()) - .ok_or_else(|| GhidraError::Other("No program specified. Use --program or set default with 'ghidra set-default program '".to_string())) -} - -/// Connect to a running bridge for a project. -fn connect_to_bridge(project_path: &Path) -> anyhow::Result { - let port = bridge::read_port_file(project_path)? - .ok_or_else(|| anyhow::anyhow!("Bridge not running for project: {}", project_path.display()))?; - Ok(BridgeClient::new(port)) -} - -/// Resolve a project name to its full path on disk. -fn resolve_project_path(project: &Option, config: &Config) -> anyhow::Result { - let project_name = project - .clone() - .or_else(|| config.default_project.clone()) - .ok_or_else(|| anyhow::anyhow!("No project specified and no default project configured"))?; - - let project_dir = config.get_project_dir()?; - - if PathBuf::from(&project_name).is_absolute() { - Ok(PathBuf::from(project_name)) - } else { - Ok(project_dir.join(project_name)) - } -} +mod cli; +mod config; +mod daemon; +mod error; +mod filter; +mod format; +mod ghidra; +mod ipc; +mod query; + +use clap::Parser; +use cli::{Cli, Commands, DaemonCommands}; +use config::Config; +use error::{GhidraError, Result}; +use format::{auto_detect_format, DefaultFormatter, Formatter, OutputFormat}; +use ghidra::bridge::{self, BridgeStartMode, BridgeStatus}; +use ghidra::GhidraClient; +use ipc::client::BridgeClient; +use std::path::{Path, PathBuf}; + +fn main() { + // Initialize logging with info level by default, can be overridden via RUST_LOG + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + let cli = Cli::parse(); + + let result = match &cli.command { + Commands::Setup(_) => { + // Setup needs async for downloading + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(run_setup(cli)) + } + Commands::Daemon(_) => handle_daemon_command_dispatch(cli), + _ => run_command(cli), + }; + + if let Err(e) = result { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} + +/// Run a command, starting the bridge if needed. +fn run_command(cli: Cli) -> anyhow::Result<()> { + match &cli.command { + // Non-bridge commands + Commands::Init => handle_init(), + Commands::Doctor => handle_doctor(), + Commands::Version => handle_version(), + Commands::Config(cmd) => handle_config_command(cmd.clone()), + Commands::SetDefault(args) => handle_set_default(args.clone()), + Commands::Project(args) => handle_project_command(args.command.clone()), + // Commands requiring bridge + _ if requires_bridge(&cli.command) => run_with_bridge(cli), + _ => { + println!("Command not yet implemented"); + Ok(()) + } + } +} + +/// Determines if a command requires the bridge to be running. +fn requires_bridge(command: &Commands) -> bool { + matches!( + command, + Commands::Import(_) + | Commands::Analyze(_) + | Commands::Quick(_) + | Commands::Query(_) + | Commands::Decompile(_) + | Commands::Function(_) + | Commands::Strings(_) + | Commands::Memory(_) + | Commands::Dump(_) + | Commands::Summary(_) + | Commands::XRef(_) + | Commands::Symbol(_) + | Commands::Type(_) + | Commands::Comment(_) + | Commands::Graph(_) + | Commands::Find(_) + | Commands::Diff(_) + | Commands::Patch(_) + | Commands::Script(_) + | Commands::Disasm(_) + | Commands::Batch(_) + | Commands::Stats(_) + | Commands::Program(_) + ) +} + +/// Extract the project name from a command's args (if present). +fn extract_project_from_command(command: &Commands) -> Option { + match command { + Commands::Import(args) => args.project.clone(), + Commands::Analyze(args) => args.project.clone(), + Commands::Quick(args) => args.project.clone(), + Commands::Query(args) => args.project.clone(), + Commands::Summary(args) => args.options.project.clone(), + Commands::Decompile(args) => args.options.project.clone(), + Commands::Function(cmd) => match cmd { + cli::FunctionCommands::List(opts) => opts.project.clone(), + cli::FunctionCommands::Decompile(args) => args.options.project.clone(), + cli::FunctionCommands::Get(args) => args.options.project.clone(), + cli::FunctionCommands::Disasm(args) => args.options.project.clone(), + cli::FunctionCommands::Calls(args) => args.options.project.clone(), + cli::FunctionCommands::XRefs(args) => args.options.project.clone(), + cli::FunctionCommands::Rename(args) => args.project.clone(), + cli::FunctionCommands::Create(args) => args.project.clone(), + cli::FunctionCommands::Delete(args) => args.options.project.clone(), + }, + Commands::Strings(cmd) => match cmd { + cli::StringsCommands::List(opts) => opts.project.clone(), + cli::StringsCommands::Refs(args) => args.options.project.clone(), + }, + Commands::Memory(cmd) => match cmd { + cli::MemoryCommands::Map(opts) => opts.project.clone(), + cli::MemoryCommands::Read(args) => args.options.project.clone(), + cli::MemoryCommands::Write(args) => args.project.clone(), + cli::MemoryCommands::Search(args) => args.options.project.clone(), + }, + Commands::Dump(cmd) => match cmd { + cli::DumpCommands::Imports(opts) => opts.project.clone(), + cli::DumpCommands::Exports(opts) => opts.project.clone(), + cli::DumpCommands::Functions(opts) => opts.project.clone(), + cli::DumpCommands::Strings(opts) => opts.project.clone(), + }, + Commands::XRef(cmd) => match cmd { + cli::XRefCommands::To(args) => args.options.project.clone(), + cli::XRefCommands::From(args) => args.options.project.clone(), + cli::XRefCommands::List(args) => args.options.project.clone(), + }, + Commands::Stats(args) => args.options.project.clone(), + Commands::Disasm(args) => args.options.project.clone(), + Commands::Find(cmd) => match cmd { + cli::FindCommands::String(args) => args.options.project.clone(), + cli::FindCommands::Bytes(args) => args.options.project.clone(), + cli::FindCommands::Function(args) => args.options.project.clone(), + cli::FindCommands::Calls(args) => args.options.project.clone(), + cli::FindCommands::Crypto(opts) => opts.project.clone(), + cli::FindCommands::Interesting(opts) => opts.project.clone(), + }, + Commands::Graph(cmd) => match cmd { + cli::GraphCommands::Calls(opts) => opts.project.clone(), + cli::GraphCommands::Callers(args) => args.options.project.clone(), + cli::GraphCommands::Callees(args) => args.options.project.clone(), + cli::GraphCommands::Export(args) => args.options.project.clone(), + }, + Commands::Comment(cmd) => match cmd { + cli::CommentCommands::List(opts) => opts.project.clone(), + cli::CommentCommands::Get(args) => args.options.project.clone(), + cli::CommentCommands::Set(args) => args.project.clone(), + cli::CommentCommands::Delete(args) => args.options.project.clone(), + }, + Commands::Symbol(cmd) => match cmd { + cli::SymbolCommands::List(opts) => opts.project.clone(), + cli::SymbolCommands::Get(args) => args.options.project.clone(), + cli::SymbolCommands::Create(args) => args.project.clone(), + cli::SymbolCommands::Delete(args) => args.options.project.clone(), + cli::SymbolCommands::Rename(args) => args.project.clone(), + }, + Commands::Type(cmd) => match cmd { + cli::TypeCommands::List(opts) => opts.project.clone(), + cli::TypeCommands::Get(args) => args.options.project.clone(), + cli::TypeCommands::Create(args) => args.project.clone(), + cli::TypeCommands::Apply(args) => args.project.clone(), + }, + Commands::Patch(cmd) => match cmd { + cli::PatchCommands::Bytes(args) => args.project.clone(), + cli::PatchCommands::Nop(args) => args.project.clone(), + cli::PatchCommands::Export(args) => args.project.clone(), + }, + Commands::Script(cmd) => match cmd { + cli::ScriptCommands::Run(args) => args.project.clone(), + cli::ScriptCommands::Python(args) => args.project.clone(), + cli::ScriptCommands::Java(args) => args.project.clone(), + cli::ScriptCommands::List => None, + }, + Commands::Program(cmd) => match cmd { + cli::ProgramCommands::List(args) => args.project.clone(), + cli::ProgramCommands::Open(args) => args.project.clone(), + cli::ProgramCommands::Close(args) => args.project.clone(), + cli::ProgramCommands::Delete(args) => args.project.clone(), + cli::ProgramCommands::Info(args) => args.project.clone(), + cli::ProgramCommands::Export(args) => args.project.clone(), + }, + _ => None, + } +} + +/// Run a command that requires the bridge. +fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { + let config = Config::load()?; + + // Extract project from command args, fall back to config default + let project_from_cmd = extract_project_from_command(&cli.command); + let project_path = resolve_project_path(&project_from_cmd, &config)?; + + let ghidra_install_dir = config + .ghidra_install_dir + .clone() + .or_else(|| config.get_ghidra_install_dir().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "Ghidra installation directory not configured. Run 'ghidra setup' first." + ) + })?; + + // For Import and Quick, we may need to start a new bridge + let client = match &cli.command { + Commands::Import(args) => { + let binary_path = PathBuf::from(&args.binary); + if !binary_path.exists() { + anyhow::bail!("Binary not found: {}", args.binary); + } + + // Check if bridge is already running + if bridge::is_bridge_running(&project_path) { + // Bridge running - import via bridge command + let port = bridge::read_port_file(&project_path)? + .ok_or_else(|| anyhow::anyhow!("Bridge port file not found"))?; + let client = BridgeClient::new(port); + let result = client.import_binary(&args.binary, args.program.as_deref())?; + + let program_name = args.program.clone().unwrap_or_else(|| { + result + .get("program") + .and_then(|p| p.as_str()) + .unwrap_or("unknown") + .to_string() + }); + + // Switch to the newly imported program + client.open_program(&program_name)?; + println!("Successfully imported as: {}", program_name); + return Ok(()); + } + + // No bridge running - start one in import mode + eprintln!("Starting Ghidra bridge..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Import { + binary_path: args.binary.clone(), + }, + )?; + let client = BridgeClient::new(port); + let info = client.program_info()?; + let program_name = args.program.clone().unwrap_or_else(|| { + info.get("name") + .and_then(|n| n.as_str()) + .unwrap_or("unknown") + .to_string() + }); + println!("Successfully imported as: {}", program_name); + return Ok(()); + } + + Commands::Quick(args) => { + let binary_path = PathBuf::from(&args.binary); + if !binary_path.exists() { + anyhow::bail!("Binary not found: {}", args.binary); + } + + println!("Quick analysis of {}...\n", args.binary); + + println!("[1/3] Importing binary..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Import { + binary_path: args.binary.clone(), + }, + )?; + let client = BridgeClient::new(port); + + client.program_info()?; + + println!("[2/3] Running analysis..."); + client.analyze()?; + + println!("[3/3] Done!\n"); + println!("Analysis complete. The bridge is running on port {}.", port); + println!("\nRun queries like:"); + println!(" ghidra function list"); + println!(" ghidra decompile main"); + println!(" ghidra summary"); + + return Ok(()); + } + + Commands::Analyze(args) => { + let program = resolve_program(&args.program, &config)?; + + // If bridge is already running, just send analyze command + if bridge::is_bridge_running(&project_path) { + let client = connect_to_bridge(&project_path)?; + println!("Analyzing {}...", program); + client.analyze()?; + println!("Analysis complete!"); + return Ok(()); + } + + // Start bridge in process mode + eprintln!("Starting Ghidra bridge..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Process { + program_name: program.clone(), + }, + )?; + let client = BridgeClient::new(port); + println!("Analyzing {}...", program); + client.analyze()?; + println!("Analysis complete!"); + return Ok(()); + } + + _ => { + // For query commands, ensure bridge is running (auto-start in process mode if needed) + if !bridge::is_bridge_running(&project_path) { + // Need a program name to start the bridge in process mode + let program = config.get_default_program().ok_or_else(|| { + anyhow::anyhow!( + "No bridge running and no default program configured.\n\ + Import a binary first: ghidra import \n\ + Or set a default: ghidra set-default program " + ) + })?; + + eprintln!("Starting Ghidra bridge..."); + let port = bridge::ensure_bridge_running( + &project_path, + &ghidra_install_dir, + BridgeStartMode::Process { + program_name: program, + }, + )?; + eprintln!("Bridge ready."); + BridgeClient::new(port) + } else { + connect_to_bridge(&project_path)? + } + } + }; + + // Execute the command via bridge + let result = execute_via_bridge(&client, &cli.command)?; + + // Determine output format based on flags and TTY detection + let format = if cli.pretty { + OutputFormat::Json + } else if cli.json { + OutputFormat::JsonCompact + } else { + auto_detect_format(atty::is(atty::Stream::Stdout)) + }; + + // Detect if result is already an array before wrapping + let values = match result { + serde_json::Value::Array(arr) => arr, + single => vec![single], + }; + + let formatter = DefaultFormatter; + let output = formatter.format(&values, format)?; + if !output.is_empty() { + println!("{}", output); + } + Ok(()) +} + +/// Execute a command via the bridge client. +fn execute_via_bridge( + client: &BridgeClient, + command: &Commands, +) -> anyhow::Result { + use serde_json::json; + + match command { + Commands::Query(args) => match args.data_type.as_str() { + "functions" => client.list_functions(args.limit, args.filter.clone()), + "strings" => client.list_strings(args.limit), + "imports" => client.list_imports(), + "exports" => client.list_exports(), + "memory" => client.memory_map(), + other => anyhow::bail!("Query type '{}' not supported", other), + }, + Commands::Decompile(args) => client.decompile(args.target.clone()), + Commands::Function(cmd) => { + use cli::FunctionCommands; + match cmd { + FunctionCommands::List(opts) => { + client.list_functions(opts.limit, opts.filter.clone()) + } + FunctionCommands::Decompile(args) => client.decompile(args.target.clone()), + FunctionCommands::Get(args) => { + client.send_command("get_function", Some(json!({"address": args.target}))) + } + FunctionCommands::Disasm(args) => client.disasm(&args.target, None), + FunctionCommands::Calls(args) => client.find_calls(&args.target), + FunctionCommands::XRefs(args) => client.xrefs_to(args.target.clone()), + FunctionCommands::Rename(args) => client.send_command( + "rename_function", + Some(json!({ + "old_name": args.old_name, + "new_name": args.new_name, + })), + ), + FunctionCommands::Create(args) => client.send_command( + "create_function", + Some(json!({ + "address": args.address, + "name": args.name, + })), + ), + FunctionCommands::Delete(args) => client.send_command( + "delete_function", + Some(json!({ + "address": args.target, + })), + ), + } + } + Commands::Strings(cmd) => { + use cli::StringsCommands; + match cmd { + StringsCommands::List(opts) => client.list_strings(opts.limit), + StringsCommands::Refs(args) => client.xrefs_to(args.string.clone()), + } + } + Commands::Memory(cmd) => { + use cli::MemoryCommands; + match cmd { + MemoryCommands::Map(_) => client.memory_map(), + MemoryCommands::Read(args) => client.send_command( + "read_memory", + Some(json!({ + "address": args.address, + "size": args.size, + })), + ), + MemoryCommands::Write(args) => client.send_command( + "write_memory", + Some(json!({ + "address": args.address, + "bytes": args.bytes, + })), + ), + MemoryCommands::Search(args) => client.send_command( + "search_memory", + Some(json!({ + "pattern": args.pattern, + })), + ), + } + } + Commands::Dump(cmd) => { + use cli::DumpCommands; + match cmd { + DumpCommands::Imports(_) => client.list_imports(), + DumpCommands::Exports(_) => client.list_exports(), + DumpCommands::Functions(opts) => { + client.list_functions(opts.limit, opts.filter.clone()) + } + DumpCommands::Strings(opts) => client.list_strings(opts.limit), + } + } + Commands::Summary(_) => client.program_info(), + Commands::XRef(cmd) => { + use cli::XRefCommands; + match cmd { + XRefCommands::To(args) => client.xrefs_to(args.address.clone()), + XRefCommands::From(args) => client.xrefs_from(args.address.clone()), + XRefCommands::List(_) => client.send_command("xrefs_list", None), + } + } + Commands::Program(cmd) => { + use cli::ProgramCommands; + match cmd { + ProgramCommands::List(_) => client.list_programs(), + ProgramCommands::Open(args) => { + let program = args.program.as_ref().ok_or_else(|| { + anyhow::anyhow!("Program name required. Use --program ") + })?; + client.open_program(program) + } + ProgramCommands::Close(_) => client.program_close(), + ProgramCommands::Delete(args) => { + let program = args + .program + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Program name required"))?; + client.program_delete(program) + } + ProgramCommands::Info(_) => client.program_info(), + ProgramCommands::Export(args) => { + client.program_export(&args.format, args.output.as_deref()) + } + } + } + Commands::Symbol(cmd) => { + use cli::SymbolCommands; + match cmd { + SymbolCommands::List(opts) => client.symbol_list(opts.filter.as_deref()), + SymbolCommands::Get(args) => client.symbol_get(&args.name), + SymbolCommands::Create(args) => client.symbol_create(&args.address, &args.name), + SymbolCommands::Delete(args) => client.symbol_delete(&args.name), + SymbolCommands::Rename(args) => { + client.symbol_rename(&args.old_name, &args.new_name) + } + } + } + Commands::Type(cmd) => { + use cli::TypeCommands; + match cmd { + TypeCommands::List(_) => client.type_list(), + TypeCommands::Get(args) => client.type_get(&args.name), + TypeCommands::Create(args) => client.type_create(&args.definition), + TypeCommands::Apply(args) => client.type_apply(&args.address, &args.type_name), + } + } + Commands::Comment(cmd) => { + use cli::CommentCommands; + match cmd { + CommentCommands::List(_) => client.comment_list(), + CommentCommands::Get(args) => client.comment_get(&args.address), + CommentCommands::Set(args) => { + client.comment_set(&args.address, &args.text, args.comment_type.as_deref()) + } + CommentCommands::Delete(args) => client.comment_delete(&args.address), + } + } + Commands::Graph(cmd) => { + use cli::GraphCommands; + match cmd { + GraphCommands::Calls(opts) => client.graph_calls(opts.limit), + GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth), + GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth), + GraphCommands::Export(args) => client.graph_export(&args.format), + } + } + Commands::Find(cmd) => { + use cli::FindCommands; + match cmd { + FindCommands::String(args) => client.find_string(&args.pattern), + FindCommands::Bytes(args) => client.find_bytes(&args.hex), + FindCommands::Function(args) => client.find_function(&args.pattern), + FindCommands::Calls(args) => client.find_calls(&args.function), + FindCommands::Crypto(_) => client.find_crypto(), + FindCommands::Interesting(_) => client.find_interesting(), + } + } + Commands::Diff(cmd) => { + use cli::DiffCommands; + match cmd { + DiffCommands::Programs(args) => { + client.diff_programs(&args.program1, &args.program2) + } + DiffCommands::Functions(args) => client.diff_functions(&args.func1, &args.func2), + } + } + Commands::Patch(cmd) => { + use cli::PatchCommands; + match cmd { + PatchCommands::Bytes(args) => client.patch_bytes(&args.address, &args.hex), + PatchCommands::Nop(args) => client.patch_nop(&args.address), + PatchCommands::Export(args) => client.patch_export(&args.output), + } + } + Commands::Script(cmd) => { + use cli::ScriptCommands; + match cmd { + ScriptCommands::Run(args) => client.script_run(&args.script_path, &args.args), + ScriptCommands::Python(args) => client.script_python(&args.code), + ScriptCommands::Java(args) => client.script_java(&args.code), + ScriptCommands::List => client.script_list(), + } + } + Commands::Disasm(args) => client.disasm(&args.address, args.num_instructions), + Commands::Batch(args) => { + // Read batch file and send commands + let content = std::fs::read_to_string(&args.script_file) + .map_err(|e| anyhow::anyhow!("Failed to read batch file: {}", e))?; + let commands: Vec = content + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#')) + .map(|l| serde_json::from_str(l).unwrap_or_else(|_| json!({"command": l.trim()}))) + .collect(); + client.batch(&commands) + } + Commands::Stats(_) => client.stats(), + _ => anyhow::bail!("Command not supported"), + } +} + +/// Dispatch daemon (bridge management) commands. +fn handle_daemon_command_dispatch(cli: Cli) -> anyhow::Result<()> { + match cli.command { + Commands::Daemon(cmd) => match cmd { + DaemonCommands::Start { + project, + program, + port: _, + foreground: _, + } => handle_bridge_start(project, program), + DaemonCommands::Stop { project } => handle_bridge_stop(project), + DaemonCommands::Restart { + project, + program, + port: _, + } => { + handle_bridge_stop(project.clone())?; + std::thread::sleep(std::time::Duration::from_secs(1)); + handle_bridge_start(project, program) + } + DaemonCommands::Status { project } => handle_bridge_status(project), + DaemonCommands::Ping { project } => handle_bridge_ping(project), + DaemonCommands::ClearCache { project: _ } => { + println!("Cache is managed by the bridge process"); + Ok(()) + } + }, + _ => unreachable!(), + } +} + +/// Start the bridge for a project. +fn handle_bridge_start(project: Option, program: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let project_path = resolve_project_path(&project, &config)?; + + let ghidra_install_dir = config + .ghidra_install_dir + .clone() + .or_else(|| config.get_ghidra_install_dir().ok()) + .ok_or_else(|| { + anyhow::anyhow!( + "Ghidra installation directory not configured. Run 'ghidra setup' first." + ) + })?; + + // Check if bridge is already running + if bridge::is_bridge_running(&project_path) { + println!( + "Bridge is already running for project: {}", + project_path.display() + ); + return Ok(()); + } + + // Determine start mode + let mode = if let Some(prog) = program { + BridgeStartMode::Process { program_name: prog } + } else { + // Need a program name + let prog = config.get_default_program().ok_or_else(|| { + anyhow::anyhow!("No program specified. Use --program or set a default.") + })?; + BridgeStartMode::Process { program_name: prog } + }; + + println!("Starting bridge for project: {}", project_path.display()); + + let port = bridge::ensure_bridge_running(&project_path, &ghidra_install_dir, mode)?; + + println!("Bridge started on port {}", port); + Ok(()) +} + +/// Stop the bridge for a project. +fn handle_bridge_stop(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let project_path = resolve_project_path(&project, &config)?; + + if bridge::is_bridge_running(&project_path) { + println!("Stopping bridge..."); + bridge::stop_bridge(&project_path)?; + println!("Bridge stopped"); + } else { + println!("No bridge running for project: {}", project_path.display()); + } + + Ok(()) +} + +/// Get bridge status for a project. +fn handle_bridge_status(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let project_path = resolve_project_path(&project, &config)?; + + match bridge::bridge_status(&project_path)? { + BridgeStatus::Running { port, pid } => { + println!("Bridge is running:"); + println!(" PID: {}", pid); + println!(" Port: {}", port); + println!(" Project: {}", project_path.display()); + } + BridgeStatus::Stopped => { + println!("No bridge running for project: {}", project_path.display()); + } + } + + Ok(()) +} + +/// Ping the bridge. +fn handle_bridge_ping(project: Option) -> anyhow::Result<()> { + let config = Config::load()?; + let project_path = resolve_project_path(&project, &config)?; + + if bridge::is_bridge_running(&project_path) { + let client = connect_to_bridge(&project_path)?; + if client.ping()? { + println!("Bridge is responsive"); + } else { + println!("Bridge is not responding"); + } + } else { + println!("No bridge running for project: {}", project_path.display()); + } + + Ok(()) +} + +/// Handle the setup command - download and install Ghidra. +async fn run_setup(cli: Cli) -> anyhow::Result<()> { + let args = match cli.command { + Commands::Setup(args) => args, + _ => unreachable!(), + }; + + println!("Ghidra Setup Wizard"); + println!("===================\n"); + + // 1. Check Java + if !args.force { + if let Err(e) = ghidra::setup::check_java_requirement() { + eprintln!("Java prerequisite check failed: {}", e); + eprintln!("Ghidra requires JDK 17+. Use --force to continue anyway."); + std::process::exit(1); + } + } else { + println!("Skipping Java check (--force specified)"); + } + + // 2. Determine Install Directory + let install_base = if let Some(d) = args.dir { + PathBuf::from(d) + } else { + dirs::data_local_dir() + .ok_or(anyhow::anyhow!("Could not determine data directory"))? + .join("ghidra-cli") + .join("ghidra") + }; + + std::fs::create_dir_all(&install_base)?; + + // 3. Install Ghidra + println!("\nInstalling to: {}", install_base.display()); + let final_path = ghidra::setup::install_ghidra(args.version, install_base).await?; + + // 4. Update Config + let mut config = Config::load()?; + config.ghidra_install_dir = Some(final_path.clone()); + config.save()?; + + println!("\nSuccess! Ghidra installed at: {}", final_path.display()); + println!("Configuration updated."); + + // 5. Verify + println!("\nVerifying installation..."); + let client = GhidraClient::new(config)?; + if client.verify_installation().is_ok() { + println!("Verification passed!"); + println!("\nYou can now run: ghidra quick "); + } else { + println!("Verification failed - analyzeHeadless not found"); + println!(" The installation may be incomplete."); + } + + Ok(()) +} + +fn handle_init() -> anyhow::Result<()> { + println!("Ghidra CLI Initialization"); + println!("========================\n"); + + let mut config = Config::default(); + + if config.ghidra_install_dir.is_none() { + println!("Ghidra installation not found automatically."); + println!("Please set GHIDRA_INSTALL_DIR environment variable or run 'ghidra setup'."); + } + + // Set default project directory + let home = dirs::home_dir().ok_or_else(|| { + GhidraError::ConfigError("Could not determine home directory".to_string()) + })?; + let project_dir = home.join(".ghidra-projects"); + config.ghidra_project_dir = Some(project_dir.clone()); + + println!("\nProject directory: {}", project_dir.display()); + + // Save config + config.save()?; + + println!( + "\nConfiguration saved to: {}", + Config::config_path()?.display() + ); + println!("\nRun 'ghidra doctor' to verify your installation."); + + Ok(()) +} + +fn handle_doctor() -> anyhow::Result<()> { + println!("Ghidra CLI Doctor"); + println!("=================\n"); + + let config = Config::load()?; + + // Check Ghidra installation + print!("Checking Ghidra installation... "); + match config.get_ghidra_install_dir() { + Ok(dir) => { + println!("OK"); + println!(" Location: {}", dir.display()); + + let client = GhidraClient::new(config.clone()); + match client { + Ok(c) => { + if c.verify_installation().is_ok() { + println!(" analyzeHeadless: OK"); + } else { + println!(" analyzeHeadless: NOT FOUND"); + } + } + Err(e) => { + println!(" Error: {}", e); + } + } + } + Err(e) => { + println!("FAILED"); + println!(" Error: {}", e); + } + } + + // Check Java + print!("\nChecking Java... "); + match ghidra::setup::check_java_requirement() { + Ok(()) => println!("OK (JDK 17+)"), + Err(e) => { + println!("FAILED"); + println!(" Error: {}", e); + } + } + + // Check project directory + print!("\nChecking project directory... "); + match config.get_project_dir() { + Ok(dir) => { + println!("OK"); + println!(" Location: {}", dir.display()); + println!( + " Exists: {}", + if dir.exists() { + "yes" + } else { + "no (will be created)" + } + ); + } + Err(e) => { + println!("FAILED"); + println!(" Error: {}", e); + } + } + + // Check config file + print!("\nConfig file... "); + match Config::config_path() { + Ok(path) => { + println!("OK"); + println!(" Location: {}", path.display()); + println!(" Exists: {}", if path.exists() { "yes" } else { "no" }); + } + Err(e) => { + println!("FAILED"); + println!(" Error: {}", e); + } + } + + println!("\nDone!"); + Ok(()) +} + +fn handle_version() -> anyhow::Result<()> { + println!("ghidra-cli {}", env!("CARGO_PKG_VERSION")); + println!("Rust CLI for Ghidra reverse engineering"); + Ok(()) +} + +fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> { + use cli::ConfigCommands; + + match cmd { + ConfigCommands::List => { + let config = Config::load()?; + println!("{}", serde_yaml::to_string(&config)?); + } + ConfigCommands::Get { key } => { + let config = Config::load()?; + let yaml = serde_yaml::to_value(&config)?; + if let Some(value) = yaml.get(&key) { + println!("{}", serde_yaml::to_string(value)?); + } else { + println!("Key not found: {}", key); + } + } + ConfigCommands::Set { key, value } => { + let mut config = Config::load()?; + match key.as_str() { + "default_output_format" => config.default_output_format = Some(value), + "timeout" => { + let timeout: u64 = value.parse().map_err(|_| { + GhidraError::ConfigError("Invalid timeout value".to_string()) + })?; + config.timeout = Some(timeout); + } + _ => { + anyhow::bail!("Unknown config key: {}", key); + } + } + config.save()?; + println!("Configuration updated"); + } + ConfigCommands::Reset => { + let config = Config::default(); + config.save()?; + println!("Configuration reset to defaults"); + } + } + + Ok(()) +} + +fn handle_set_default(args: cli::SetDefaultArgs) -> anyhow::Result<()> { + let mut config = Config::load()?; + + match args.kind.as_str() { + "program" => { + config.default_program = Some(args.value.clone()); + config.save()?; + println!("Default program set to: {}", args.value); + } + "project" => { + config.default_project = Some(args.value.clone()); + config.save()?; + println!("Default project set to: {}", args.value); + } + _ => { + anyhow::bail!(format!("Unknown default kind: {}", args.kind)); + } + } + + Ok(()) +} + +fn handle_project_command(cmd: cli::ProjectCommands) -> anyhow::Result<()> { + use cli::ProjectCommands; + + let config = Config::load()?; + let client = GhidraClient::new(config)?; + + match cmd { + ProjectCommands::Create { name } => { + client.create_project(&name)?; + println!("Project '{}' created", name); + } + ProjectCommands::List => { + let project_dir = client.get_project_dir(); + if !project_dir.exists() { + println!("No projects found"); + return Ok(()); + } + + println!("Projects:"); + for entry in std::fs::read_dir(project_dir)? { + let entry = entry?; + if entry.path().is_dir() { + if let Some(name) = entry.file_name().to_str() { + println!(" {}", name); + } + } + } + } + ProjectCommands::Delete { name } => { + let project_path = client.get_project_path(&name); + if project_path.exists() { + std::fs::remove_dir_all(&project_path)?; + println!("Project '{}' deleted", name); + } else { + println!("Project '{}' not found", name); + } + } + ProjectCommands::Info { name } => { + let project_name = name.unwrap_or_else(|| "default".to_string()); + let project_path = client.get_project_path(&project_name); + println!("Project: {}", project_name); + println!("Path: {}", project_path.display()); + println!("Exists: {}", project_path.exists()); + } + } + + Ok(()) +} + +fn resolve_program(program: &Option, config: &Config) -> Result { + program + .clone() + .or_else(|| config.get_default_program()) + .ok_or_else(|| GhidraError::Other("No program specified. Use --program or set default with 'ghidra set-default program '".to_string())) +} + +/// Connect to a running bridge for a project. +fn connect_to_bridge(project_path: &Path) -> anyhow::Result { + let port = bridge::read_port_file(project_path)?.ok_or_else(|| { + anyhow::anyhow!("Bridge not running for project: {}", project_path.display()) + })?; + Ok(BridgeClient::new(port)) +} + +/// Resolve a project name to its full path on disk. +fn resolve_project_path(project: &Option, config: &Config) -> anyhow::Result { + let project_name = project + .clone() + .or_else(|| config.default_project.clone()) + .ok_or_else(|| anyhow::anyhow!("No project specified and no default project configured"))?; + + let project_dir = config.get_project_dir()?; + + if PathBuf::from(&project_name).is_absolute() { + Ok(PathBuf::from(project_name)) + } else { + Ok(project_dir.join(project_name)) + } +} diff --git a/tests/batch_tests.rs b/tests/batch_tests.rs index f1a4f38..4afe6ed 100644 --- a/tests/batch_tests.rs +++ b/tests/batch_tests.rs @@ -38,7 +38,8 @@ query --function main Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -69,7 +70,8 @@ fn test_batch_empty_file() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -100,7 +102,8 @@ query --address 0x100000 Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() @@ -122,7 +125,8 @@ fn test_batch_invalid_file() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("batch") .arg("/nonexistent/batch/file.txt") .assert() @@ -150,7 +154,8 @@ query --address 0x100000 Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("batch") .arg(batch_file.to_str().unwrap()) .assert() diff --git a/tests/comment_tests.rs b/tests/comment_tests.rs index 30761fd..fb0947e 100644 --- a/tests/comment_tests.rs +++ b/tests/comment_tests.rs @@ -23,7 +23,8 @@ fn test_comment_set_and_get() { // Note: ELF entry is 0x18910, but Ghidra loads with base 0x100000 Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118910") @@ -36,7 +37,8 @@ fn test_comment_set_and_get() { // Get the comment back Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("get") .arg("0x00118910") @@ -59,7 +61,8 @@ fn test_comment_list() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118920") // Within executable range (Ghidra address space) @@ -71,7 +74,8 @@ fn test_comment_list() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("list") .arg("--program") @@ -93,7 +97,8 @@ fn test_comment_delete() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("set") .arg("0x00118930") // Within executable range (Ghidra address space) @@ -105,7 +110,8 @@ fn test_comment_delete() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("comment") .arg("delete") .arg("0x00118930") // Within executable range (Ghidra address space) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 960505b..f814b18 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -119,7 +119,8 @@ impl DaemonTestHarness { .join(project); // Start the bridge using the CLI command (which starts Ghidra headless) - let mut cmd = assert_cmd::Command::cargo_bin("ghidra").expect("Failed to find ghidra binary"); + let mut cmd = + assert_cmd::Command::cargo_bin("ghidra").expect("Failed to find ghidra binary"); let result = cmd .arg("daemon") .arg("start") @@ -134,7 +135,11 @@ impl DaemonTestHarness { if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); let stdout = String::from_utf8_lossy(&result.stdout); - anyhow::bail!("Failed to start bridge:\nstdout: {}\nstderr: {}", stdout, stderr); + anyhow::bail!( + "Failed to start bridge:\nstdout: {}\nstderr: {}", + stdout, + stderr + ); } // Read port from port file @@ -157,7 +162,10 @@ impl DaemonTestHarness { let data_dir = dirs::data_local_dir() .context("Could not determine data directory")? .join("ghidra-cli"); - let hash = format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes())); + let hash = format!( + "{:x}", + md5::compute(project_path.to_string_lossy().as_bytes()) + ); let port_file = data_dir.join(format!("bridge-{}.port", hash)); while start.elapsed() < timeout { diff --git a/tests/daemon_tests.rs b/tests/daemon_tests.rs index 64d83df..c69e645 100644 --- a/tests/daemon_tests.rs +++ b/tests/daemon_tests.rs @@ -23,11 +23,10 @@ fn test_daemon_start() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("status") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("status") .assert() .success(); @@ -46,11 +45,10 @@ fn test_daemon_status() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("status") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("status") .assert() .success() .stdout(predicate::str::contains("running")); @@ -70,11 +68,10 @@ fn test_daemon_ping() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("ping") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("ping") .assert() .success(); @@ -93,11 +90,10 @@ fn test_daemon_clear_cache() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("clear-cache") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("clear-cache") .assert() .success(); @@ -116,32 +112,29 @@ fn test_daemon_lifecycle() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("status") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("status") .assert() .success() .stdout(predicate::str::contains("running")); Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("ping") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("ping") .assert() .success(); Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("stop") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("stop") .assert() .success(); } @@ -158,21 +151,19 @@ fn test_daemon_stop() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("stop") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("stop") .assert() .success(); Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("status") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("status") .assert() .success() .stdout(predicate::str::contains("No daemon running")); @@ -192,11 +183,10 @@ fn test_daemon_restart() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("restart") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("restart") .arg("--program") .arg(TEST_PROGRAM) .assert() @@ -204,11 +194,10 @@ fn test_daemon_restart() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("stop") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("stop") .assert() .success(); @@ -227,11 +216,10 @@ fn test_daemon_start_when_running() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) - .arg("daemon") - .arg("start") .arg("--project") .arg(TEST_PROJECT) + .arg("daemon") + .arg("start") .arg("--program") .arg(TEST_PROGRAM) .assert() diff --git a/tests/diff_tests.rs b/tests/diff_tests.rs index 323c00d..78d0f72 100644 --- a/tests/diff_tests.rs +++ b/tests/diff_tests.rs @@ -22,7 +22,8 @@ fn test_diff_programs() { // diff programs compares two programs by name (no --program flag needed) Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("diff") .arg("programs") .arg(TEST_PROGRAM) @@ -45,7 +46,8 @@ fn test_diff_functions() { // Using _start (entry point) for both since we just want to verify command works Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("diff") .arg("functions") .arg("_start") diff --git a/tests/find_tests.rs b/tests/find_tests.rs index 43bc2de..0641a65 100644 --- a/tests/find_tests.rs +++ b/tests/find_tests.rs @@ -21,7 +21,8 @@ fn test_find_string() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("string") .arg("test") @@ -44,7 +45,8 @@ fn test_find_bytes() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("bytes") .arg("4883ec08") @@ -67,7 +69,8 @@ fn test_find_function() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("function") .arg("main") @@ -90,7 +93,8 @@ fn test_find_function_glob() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("function") .arg("m*") @@ -113,7 +117,8 @@ fn test_find_calls() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("calls") .arg("printf") @@ -135,7 +140,8 @@ fn test_find_crypto() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("crypto") .arg("--program") @@ -157,7 +163,8 @@ fn test_find_interesting() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("interesting") .arg("--program") @@ -179,7 +186,8 @@ fn test_find_string_no_matches() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("find") .arg("string") .arg("nonexistent_string_xyz123") diff --git a/tests/graph_tests.rs b/tests/graph_tests.rs index 1e6a593..7962364 100644 --- a/tests/graph_tests.rs +++ b/tests/graph_tests.rs @@ -21,7 +21,8 @@ fn test_graph_calls() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("graph") .arg("calls") .arg("--program") @@ -44,7 +45,8 @@ fn test_graph_callers() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("graph") .arg("callers") .arg("main") @@ -67,7 +69,8 @@ fn test_graph_callees() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("graph") .arg("callees") .arg("main") @@ -90,7 +93,8 @@ fn test_graph_export_dot() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("graph") .arg("export") .arg("dot") diff --git a/tests/program_tests.rs b/tests/program_tests.rs index 2099cc7..4829b07 100644 --- a/tests/program_tests.rs +++ b/tests/program_tests.rs @@ -21,7 +21,8 @@ fn test_program_info() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("program") .arg("info") .arg("--program") @@ -44,7 +45,8 @@ fn test_program_export_json() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("program") .arg("export") .arg("json") @@ -67,7 +69,8 @@ fn test_program_close() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("program") .arg("close") .arg("--program") @@ -88,7 +91,8 @@ fn test_program_info_no_program() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("program") .arg("info") .assert() diff --git a/tests/reliability_tests.rs b/tests/reliability_tests.rs index f54400f..be8331e 100644 --- a/tests/reliability_tests.rs +++ b/tests/reliability_tests.rs @@ -28,7 +28,8 @@ fn test_stale_files_cleaned_on_restart() { // Verify bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("daemon") .arg("ping") .timeout(Duration::from_secs(30)) @@ -49,7 +50,8 @@ fn test_stale_files_cleaned_on_restart() { // Verify bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("daemon") .arg("ping") .timeout(Duration::from_secs(30)) @@ -70,13 +72,14 @@ fn test_recovery_after_crash() { // Start bridge and verify it works { - let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) - .expect("Failed to start bridge"); + let harness = + DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start bridge"); // Verify it's working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("daemon") .arg("ping") .timeout(Duration::from_secs(30)) @@ -97,7 +100,8 @@ fn test_recovery_after_crash() { // Verify new bridge is working assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("daemon") .arg("ping") .timeout(Duration::from_secs(30)) @@ -120,7 +124,8 @@ fn test_bridge_not_ready_error() { // Ping should work assert_cmd::Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("daemon") .arg("ping") .timeout(Duration::from_secs(30)) diff --git a/tests/script_tests.rs b/tests/script_tests.rs index 36173cc..64dfa5a 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -46,7 +46,8 @@ fn test_script_list() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("script") .arg("list") .arg("--program") @@ -70,7 +71,8 @@ fn test_script_run() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("script") .arg("run") .arg(script_path.to_str().unwrap()) @@ -95,7 +97,8 @@ fn test_script_python_inline() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("script") .arg("python") .arg("output = 'Hello from Python'") @@ -118,7 +121,8 @@ fn test_script_run_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("script") .arg("run") .arg("/nonexistent/script.py") diff --git a/tests/stats_tests.rs b/tests/stats_tests.rs index 7639dc6..a95c5fa 100644 --- a/tests/stats_tests.rs +++ b/tests/stats_tests.rs @@ -21,7 +21,8 @@ fn test_stats_normal() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) @@ -44,7 +45,8 @@ fn test_stats_has_all_fields() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) @@ -72,7 +74,8 @@ fn test_stats_json_format() { let output = Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("stats") .arg("--program") .arg(TEST_PROGRAM) diff --git a/tests/symbol_tests.rs b/tests/symbol_tests.rs index c8a64e1..5bc5f93 100644 --- a/tests/symbol_tests.rs +++ b/tests/symbol_tests.rs @@ -21,7 +21,8 @@ fn test_symbol_list() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("list") .arg("--program") @@ -43,7 +44,8 @@ fn test_symbol_create_and_get() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("create") .arg("0x1000") @@ -55,7 +57,8 @@ fn test_symbol_create_and_get() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("get") .arg("test_symbol") @@ -78,7 +81,8 @@ fn test_symbol_rename() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("create") .arg("0x2000") @@ -90,7 +94,8 @@ fn test_symbol_rename() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("rename") .arg("old_symbol") @@ -113,7 +118,8 @@ fn test_symbol_get_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("symbol") .arg("get") .arg("nonexistent_symbol_12345") diff --git a/tests/type_tests.rs b/tests/type_tests.rs index 68d7c68..6e10809 100644 --- a/tests/type_tests.rs +++ b/tests/type_tests.rs @@ -21,7 +21,8 @@ fn test_type_list() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("type") .arg("list") .arg("--program") @@ -43,7 +44,8 @@ fn test_type_get_primitive() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("type") .arg("get") .arg("int") @@ -66,7 +68,8 @@ fn test_type_create() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("type") .arg("create") .arg("MyTestStruct") @@ -88,7 +91,8 @@ fn test_type_apply() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("type") .arg("apply") .arg("0x1000") @@ -111,7 +115,8 @@ fn test_type_get_nonexistent() { Command::cargo_bin("ghidra") .unwrap() - .arg("--project").arg(TEST_PROJECT) + .arg("--project") + .arg(TEST_PROJECT) .arg("type") .arg("get") .arg("NonexistentType12345")