From af315c20cc645771b0d78be4297fb0ae8f0a2bc1 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Mon, 23 Feb 2026 16:01:11 -0800 Subject: [PATCH] docs updates --- README.md | 64 +++--- docs/plan-complete-stub-commands.md | 3 + docs/plan-e2e-tests.md | 3 + docs/plan-prod.md | 3 + src/cli.rs | 124 +++++++++++- src/ghidra/scripts/GhidraCliBridge.java | 257 +++++++++++++++++++++++- src/main.rs | 37 ++-- tests/README.md | 2 +- 8 files changed, 433 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index b433378..a8f10d2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ghidra CLI +# Ghidra CLI A high-performance Rust CLI for automating Ghidra reverse engineering tasks, designed for both direct usage and AI agent integration (like Claude Code). @@ -78,9 +78,9 @@ ghidra find string "password" # Get cross-references ghidra x-ref to 0x401000 -# Generate call graph -ghidra graph calls main --depth 3 -``` +# Generate call graph +ghidra graph callers main --depth 3 +``` ## Commands @@ -103,11 +103,11 @@ ghidra disasm
--instructions 20 # Disassemble instructions ### Symbols & Types ```bash -ghidra symbol list # List symbols -ghidra symbol create # Create symbol -ghidra symbol rename # Rename symbol -ghidra type list # List data types -ghidra type get # Get type details +ghidra symbol list # List symbols +ghidra symbol create # Create symbol +ghidra symbol rename # Rename symbol +ghidra type list # List data types +ghidra type get # Get type details ``` ### Cross-References @@ -125,27 +125,31 @@ ghidra find crypto # Find crypto constants ghidra find interesting # Find interesting patterns ``` -### Call Graphs -```bash -ghidra graph calls # Full call graph -ghidra graph callers # Who calls this? -ghidra graph callees # What does this call? -ghidra graph export dot # Export to DOT format -``` +### Call Graphs +```bash +ghidra graph calls # Full call graph +ghidra graph callers # Who calls this? (--depth optional) +ghidra graph callees # What does this call? (--depth optional) +ghidra graph export dot # Export to DOT format +``` -### Binary Patching -```bash -ghidra patch bytes "90 90" # Patch bytes -ghidra patch nop --count 5 # NOP out instructions -ghidra patch export # Export as patch file -``` +### Binary Patching +```bash +ghidra patch bytes "90 90" # Patch bytes +ghidra patch nop --count 5 # NOP out instructions +ghidra patch export -o patched.bin # Export patched binary +``` + +Note: `patch nop --count` is currently parsed by the CLI, but runtime uses single-address NOP behavior. -### Comments -```bash -ghidra comment get
# Get comment -ghidra comment set "note" # Set comment -ghidra comment list # List all comments -``` +### Comments +```bash +ghidra comment get
# Get comment +ghidra comment set "note" --comment-type EOL # Set comment +ghidra comment list # List all comments +``` + +Note: `--comment-type` currently falls back to `EOL` due client/bridge argument key mismatch. ### Scripts ```bash @@ -247,8 +251,8 @@ Example workflow with an AI agent: 2. `ghidra find interesting` - AI analyzes suspicious patterns 3. `ghidra decompile ` - AI examines specific functions 4. `ghidra x-ref to ` - AI traces data flow -5. `ghidra patch nop ` - AI patches anti-debug code -6. `ghidra patch export` - Export patched binary +5. `ghidra patch nop ` - AI patches anti-debug code +6. `ghidra patch export -o patched.bin` - Export patched binary ## Troubleshooting diff --git a/docs/plan-complete-stub-commands.md b/docs/plan-complete-stub-commands.md index 35f55d9..38482cf 100644 --- a/docs/plan-complete-stub-commands.md +++ b/docs/plan-complete-stub-commands.md @@ -1,5 +1,8 @@ # Plan: Complete All Stub Commands +> Historical planning document: parts of this plan assume an older daemon-centric architecture and may not match the current implementation. +> For current behavior, use `README.md`, `AGENTS.md`, and the CLI help output. + ## Overview ghidra-cli has 39 stub commands that output "not yet implemented". This plan implements all stub commands using a hybrid approach: grouped by category with shared helpers per group. Each category becomes a deployable unit with its own Ghidra bridge Python scripts, daemon routing, and E2E tests. diff --git a/docs/plan-e2e-tests.md b/docs/plan-e2e-tests.md index a9f4903..9d60264 100644 --- a/docs/plan-e2e-tests.md +++ b/docs/plan-e2e-tests.md @@ -1,5 +1,8 @@ # E2E Test Coverage Plan +> Historical planning document: this plan captures prior design decisions and may not match current test architecture exactly. +> For current test behavior and commands, see `tests/README.md`. + ## Overview This plan addresses the critical E2E test coverage gap in ghidra-cli. Currently only 4 of 60+ CLI commands have active tests. The plan implements a modular test structure with daemon lifecycle management, enabling comprehensive testing of all CLI functionality including the 51+ untested commands. diff --git a/docs/plan-prod.md b/docs/plan-prod.md index ff5b393..c159af3 100644 --- a/docs/plan-prod.md +++ b/docs/plan-prod.md @@ -1,5 +1,8 @@ # Ghidra-CLI Open Source Release Plan (Daemon-Only Architecture) +> Historical planning document: this does not reflect the current implementation. +> Current architecture is direct CLI-to-Java bridge (`GhidraCliBridge.java`) as documented in `README.md` and `AGENTS.md`. + ## Overview Prepare ghidra-cli for open source release with a **daemon-only architecture**. Binary analysis is slow enough that a persistent Ghidra process (via daemon) is always preferable to spawning new processes per command. diff --git a/src/cli.rs b/src/cli.rs index fefcf76..551f610 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -312,12 +312,25 @@ pub enum FunctionCommands { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FunctionGetArgs { - /// Function address or name - pub target: String, + /// Function target (name/address/FUN_...) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// Function target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, #[command(flatten)] pub options: QueryOptions, } +impl FunctionGetArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct RenameArgs { pub old_name: String, @@ -436,11 +449,25 @@ pub enum XRefCommands { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct XRefArgs { - pub address: String, + /// XRef target (name | 0xaddr | FUN_) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// XRef target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, #[command(flatten)] pub options: QueryOptions, } +impl XRefArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum TypeCommands { /// List data types @@ -555,11 +582,25 @@ pub struct FindFunctionArgs { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct FindCallsArgs { - pub function: String, + /// Function target (name | 0xaddr | FUN_) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// Function target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, #[command(flatten)] pub options: QueryOptions, } +impl FindCallsArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum GraphCommands { /// Call graph @@ -576,13 +617,27 @@ pub enum GraphCommands { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct GraphFunctionArgs { - pub function: String, + /// Function target (name | 0xaddr | FUN_) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// Function target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, #[arg(long)] pub depth: Option, #[command(flatten)] pub options: QueryOptions, } +impl GraphFunctionArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct GraphExportArgs { /// Export format (e.g., dot, json) @@ -594,14 +649,33 @@ pub struct GraphExportArgs { #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct DecompileArgs { - pub target: String, + /// Function target (name | 0xaddr | FUN_) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// Function target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, #[command(flatten)] pub options: QueryOptions, } +impl DecompileArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Args, Clone, Serialize, Deserialize, Debug)] pub struct DisasmArgs { - pub address: String, + /// Disassembly target (name | 0xaddr | FUN_) + #[arg(value_name = "TARGET", required_unless_present = "target")] + pub positional_target: Option, + /// Disassembly target (name | 0xaddr | FUN_) + #[arg(long = "target", value_name = "TARGET")] + pub target: Option, /// Number of instructions to disassemble #[arg(long = "instructions", short = 'n')] pub num_instructions: Option, @@ -609,6 +683,15 @@ pub struct DisasmArgs { pub options: QueryOptions, } +impl DisasmArgs { + pub fn resolved_target(&self) -> &str { + self.target + .as_deref() + .or(self.positional_target.as_deref()) + .expect("clap should ensure target is provided") + } +} + #[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] pub enum DiffCommands { /// Compare two programs @@ -838,3 +921,30 @@ pub struct SetupArgs { #[arg(long)] pub force: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_decompile_target_flag() { + let cli = Cli::try_parse_from(["ghidra", "decompile", "--target", "FUN_00401000"]) + .expect("decompile --target should parse"); + match cli.command { + Commands::Decompile(args) => assert_eq!(args.resolved_target(), "FUN_00401000"), + _ => panic!("expected decompile command"), + } + } + + #[test] + fn parses_function_get_positional_target() { + let cli = Cli::try_parse_from(["ghidra", "function", "get", "main"]) + .expect("function get positional target should parse"); + match cli.command { + Commands::Function(FunctionCommands::Get(args)) => { + assert_eq!(args.resolved_target(), "main"); + } + _ => panic!("expected function get command"), + } + } +} diff --git a/src/ghidra/scripts/GhidraCliBridge.java b/src/ghidra/scripts/GhidraCliBridge.java index 2b3cb5b..3669ad4 100644 --- a/src/ghidra/scripts/GhidraCliBridge.java +++ b/src/ghidra/scripts/GhidraCliBridge.java @@ -30,6 +30,7 @@ import ghidra.util.task.TaskMonitor; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -188,6 +189,10 @@ public class GhidraCliBridge extends GhidraScript { case "ping": return handlePing(); case "program_info": return handleProgramInfo(); case "list_functions": return handleListFunctions(args); + case "get_function": return handleGetFunction(args); + case "rename_function": return handleRenameFunction(args); + case "create_function": return handleCreateFunction(args); + case "delete_function": return handleDeleteFunction(args); case "decompile": return handleDecompile(args); case "list_strings": return handleListStrings(args); case "list_imports": return handleListImports(); @@ -479,6 +484,242 @@ public class GhidraCliBridge extends GhidraScript { return result; } + private JsonObject functionToJson(Function func) { + JsonObject funcData = new JsonObject(); + funcData.addProperty("name", func.getName()); + funcData.addProperty("address", func.getEntryPoint().toString()); + funcData.addProperty("size", func.getBody().getNumAddresses()); + funcData.addProperty("entry_point", func.getEntryPoint().toString()); + + String sig = null; + try { + sig = func.getPrototypeString(false, false); + } catch (Exception e) { + // ignore + } + if (sig != null) { + funcData.addProperty("signature", sig); + } else { + funcData.add("signature", JsonNull.INSTANCE); + } + + funcData.addProperty("calling_convention", func.getCallingConventionName()); + + String comment = func.getComment(); + if (comment != null) { + funcData.addProperty("comment", comment); + } else { + funcData.add("comment", JsonNull.INSTANCE); + } + + return funcData; + } + + private String buildFunctionTargetHint(String target) { + if (currentProgram == null || target == null || target.isEmpty()) { + return "Function not found"; + } + + String query = target.toLowerCase(); + List containsMatches = new ArrayList<>(); + List fuzzyMatches = new ArrayList<>(); + FunctionIterator iter = currentProgram.getFunctionManager().getFunctions(true); + + while (iter.hasNext()) { + Function func = iter.next(); + String name = func.getName(); + String lname = name.toLowerCase(); + + if (lname.contains(query)) { + containsMatches.add(name); + } else if (query.length() >= 3 && levenshteinDistance(lname, query) <= 3) { + fuzzyMatches.add(name); + } + } + + Collections.sort(containsMatches); + Collections.sort(fuzzyMatches); + + List suggestions = new ArrayList<>(); + for (String name : containsMatches) { + suggestions.add(name); + if (suggestions.size() >= 5) break; + } + if (suggestions.size() < 5) { + for (String name : fuzzyMatches) { + if (!suggestions.contains(name)) suggestions.add(name); + if (suggestions.size() >= 5) break; + } + } + + StringBuilder hint = new StringBuilder(); + hint.append("Cannot resolve function target: ").append(target) + .append(". Try: ghidra function list --filter ").append(target); + if (!suggestions.isEmpty()) { + hint.append(". Closest matches: ").append(String.join(", ", suggestions)); + } + return hint.toString(); + } + + private int levenshteinDistance(String a, String b) { + int n = a.length(); + int m = b.length(); + int[][] dp = new int[n + 1][m + 1]; + + for (int i = 0; i <= n; i++) dp[i][0] = i; + for (int j = 0; j <= m; j++) dp[0][j] = j; + + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1; + dp[i][j] = Math.min( + Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), + dp[i - 1][j - 1] + cost + ); + } + } + return dp[n][m]; + } + + private JsonObject handleGetFunction(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String target = getArgString(args, "address"); + if (target == null || target.isEmpty()) { + return errorResult("Function target required"); + } + + Address addr = resolveAddress(target); + if (addr == null) { + return errorResult(buildFunctionTargetHint(target)); + } + + Function func = currentProgram.getFunctionManager().getFunctionContaining(addr); + if (func == null) { + return errorResult("No function at target " + target + ". Try: ghidra function list --filter " + target); + } + return functionToJson(func); + } + + private JsonObject handleRenameFunction(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String oldTarget = getArgString(args, "old_name"); + String newName = getArgString(args, "new_name"); + if (oldTarget == null || newName == null || oldTarget.isEmpty() || newName.isEmpty()) { + return errorResult("old_name and new_name required"); + } + + try { + Function func = findFunctionByNameOrAddress(oldTarget); + if (func == null) { + return errorResult(buildFunctionTargetHint(oldTarget)); + } + + int txId = currentProgram.startTransaction("Rename function"); + try { + String oldName = func.getName(); + func.setName(newName, SourceType.USER_DEFINED); + currentProgram.endTransaction(txId, true); + + JsonObject result = new JsonObject(); + result.addProperty("status", "renamed"); + result.addProperty("old_name", oldName); + result.addProperty("new_name", newName); + result.addProperty("address", func.getEntryPoint().toString()); + return result; + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + } catch (Exception e) { + return errorResult("Failed to rename function: " + e.getMessage()); + } + } + + private JsonObject handleCreateFunction(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String target = getArgString(args, "address"); + String requestedName = getArgString(args, "name"); + if (target == null || target.isEmpty()) { + return errorResult("Function target required"); + } + + try { + Address addr = resolveAddress(target); + if (addr == null) { + return errorResult("Invalid function target: " + target + ". Expected address/symbol/FUN_."); + } + + FunctionManager fm = currentProgram.getFunctionManager(); + if (fm.getFunctionContaining(addr) != null) { + return errorResult("Function already exists at " + addr.toString()); + } + + String functionName = (requestedName == null || requestedName.isEmpty()) + ? ("FUN_" + addr.toString().replace(":", "")) + : requestedName; + + int txId = currentProgram.startTransaction("Create function"); + try { + Function created = fm.createFunction(functionName, addr, null, SourceType.USER_DEFINED); + if (created == null) { + currentProgram.endTransaction(txId, false); + return errorResult("Failed to create function at " + addr.toString()); + } + currentProgram.endTransaction(txId, true); + + JsonObject result = new JsonObject(); + result.addProperty("status", "created"); + result.addProperty("name", created.getName()); + result.addProperty("address", created.getEntryPoint().toString()); + return result; + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + } catch (Exception e) { + return errorResult("Failed to create function: " + e.getMessage()); + } + } + + private JsonObject handleDeleteFunction(JsonObject args) { + if (currentProgram == null) return errorResult("No program loaded"); + + String target = getArgString(args, "address"); + if (target == null || target.isEmpty()) { + return errorResult("Function target required"); + } + + try { + FunctionManager fm = currentProgram.getFunctionManager(); + Function func = findFunctionByNameOrAddress(target); + if (func == null) { + return errorResult(buildFunctionTargetHint(target)); + } + + Address entry = func.getEntryPoint(); + String name = func.getName(); + int txId = currentProgram.startTransaction("Delete function"); + try { + fm.removeFunction(entry); + currentProgram.endTransaction(txId, true); + } catch (Exception e) { + currentProgram.endTransaction(txId, false); + throw e; + } + + JsonObject result = new JsonObject(); + result.addProperty("status", "deleted"); + result.addProperty("name", name); + result.addProperty("address", entry.toString()); + return result; + } catch (Exception e) { + return errorResult("Failed to delete function: " + e.getMessage()); + } + } + private JsonObject handleDecompile(JsonObject args) { if (currentProgram == null) { return errorResult("No program loaded"); @@ -491,7 +732,7 @@ public class GhidraCliBridge extends GhidraScript { Address addr = resolveAddress(addrStr); if (addr == null) { - return errorResult("Cannot resolve address or function name: " + addrStr); + return errorResult(buildFunctionTargetHint(addrStr)); } FunctionManager fm = currentProgram.getFunctionManager(); @@ -672,7 +913,7 @@ public class GhidraCliBridge extends GhidraScript { Address addr = resolveAddress(addrStr); if (addr == null) { - return errorResult("Cannot resolve address or function name: " + addrStr); + return errorResult(buildFunctionTargetHint(addrStr)); } JsonArray xrefs = new JsonArray(); @@ -719,7 +960,7 @@ public class GhidraCliBridge extends GhidraScript { Address addr = resolveAddress(addrStr); if (addr == null) { - return errorResult("Cannot resolve address or function name: " + addrStr); + return errorResult(buildFunctionTargetHint(addrStr)); } JsonArray xrefs = new JsonArray(); @@ -1310,7 +1551,7 @@ public class GhidraCliBridge extends GhidraScript { Function targetFunc = findFunctionByNameOrAddress(functionTarget); if (targetFunc == null) { - return errorResult("Function not found: " + functionTarget); + return errorResult(buildFunctionTargetHint(functionTarget)); } ReferenceManager refMgr = currentProgram.getReferenceManager(); @@ -2100,7 +2341,7 @@ public class GhidraCliBridge extends GhidraScript { int depth = getArgInt(args, "depth", 1); Function targetFunc = findFunctionByNameOrAddress(funcName); - if (targetFunc == null) return errorResult("Function not found: " + funcName); + if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName)); ReferenceManager refMgr = currentProgram.getReferenceManager(); FunctionManager fm = currentProgram.getFunctionManager(); @@ -2151,7 +2392,7 @@ public class GhidraCliBridge extends GhidraScript { int depth = getArgInt(args, "depth", 1); Function targetFunc = findFunctionByNameOrAddress(funcName); - if (targetFunc == null) return errorResult("Function not found: " + funcName); + if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName)); ReferenceManager refMgr = currentProgram.getReferenceManager(); FunctionManager fm = currentProgram.getFunctionManager(); @@ -2300,8 +2541,8 @@ public class GhidraCliBridge extends GhidraScript { Function func1 = findFunctionByNameOrAddress(func1Target); Function func2 = findFunctionByNameOrAddress(func2Target); - if (func1 == null) return errorResult("Function not found: " + func1Target); - if (func2 == null) return errorResult("Function not found: " + func2Target); + if (func1 == null) return errorResult(buildFunctionTargetHint(func1Target)); + if (func2 == null) return errorResult(buildFunctionTargetHint(func2Target)); DecompInterface decompiler = new DecompInterface(); try { diff --git a/src/main.rs b/src/main.rs index e0d37d3..5dfe5b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -628,20 +628,25 @@ fn execute_via_bridge( "memory" => client.memory_map(), other => anyhow::bail!("Query type '{}' not supported", other), }, - Commands::Decompile(args) => client.decompile(args.target.clone()), + Commands::Decompile(args) => client.decompile(args.resolved_target().to_string()), Commands::Function(cmd) => { use cli::FunctionCommands; match cmd { FunctionCommands::List(opts) => { client.list_functions(opts.limit.or(default_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::Decompile(args) => { + client.decompile(args.resolved_target().to_string()) } - 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::Get(args) => { + client.send_command( + "get_function", + Some(json!({"address": args.resolved_target()})), + ) + } + FunctionCommands::Disasm(args) => client.disasm(args.resolved_target(), None), + FunctionCommands::Calls(args) => client.find_calls(args.resolved_target()), + FunctionCommands::XRefs(args) => client.xrefs_to(args.resolved_target().to_string()), FunctionCommands::Rename(args) => client.send_command( "rename_function", Some(json!({ @@ -659,7 +664,7 @@ fn execute_via_bridge( FunctionCommands::Delete(args) => client.send_command( "delete_function", Some(json!({ - "address": args.target, + "address": args.resolved_target(), })), ), } @@ -712,8 +717,8 @@ fn execute_via_bridge( 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::To(args) => client.xrefs_to(args.resolved_target().to_string()), + XRefCommands::From(args) => client.xrefs_from(args.resolved_target().to_string()), XRefCommands::List(_) => client.send_command("xrefs_list", None), } } @@ -777,8 +782,12 @@ fn execute_via_bridge( use cli::GraphCommands; match cmd { GraphCommands::Calls(opts) => client.graph_calls(opts.limit.or(default_limit)), - GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth), - GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth), + GraphCommands::Callers(args) => { + client.graph_callers(args.resolved_target(), args.depth) + } + GraphCommands::Callees(args) => { + client.graph_callees(args.resolved_target(), args.depth) + } GraphCommands::Export(args) => client.graph_export(&args.format), } } @@ -788,7 +797,7 @@ fn execute_via_bridge( 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::Calls(args) => client.find_calls(args.resolved_target()), FindCommands::Crypto(_) => client.find_crypto(), FindCommands::Interesting(_) => client.find_interesting(), } @@ -819,7 +828,7 @@ fn execute_via_bridge( ScriptCommands::List => client.script_list(), } } - Commands::Disasm(args) => client.disasm(&args.address, args.num_instructions), + Commands::Disasm(args) => client.disasm(args.resolved_target(), args.num_instructions), Commands::Batch(args) => { // Read batch file and execute each command locally let content = std::fs::read_to_string(&args.script_file) diff --git a/tests/README.md b/tests/README.md index db1961e..30dc5c9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -76,7 +76,7 @@ cargo test --test command_tests test_version Run tests that don't need Ghidra: ```bash -cargo test --test e2e --test command_tests --test output_format_integration +cargo test --test e2e --test output_format_integration ``` ## Test Requirements