mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
docs updates
This commit is contained in:
@@ -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).
|
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
|
# Get cross-references
|
||||||
ghidra x-ref to 0x401000
|
ghidra x-ref to 0x401000
|
||||||
|
|
||||||
# Generate call graph
|
# Generate call graph
|
||||||
ghidra graph calls main --depth 3
|
ghidra graph callers main --depth 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -103,11 +103,11 @@ ghidra disasm <address> --instructions 20 # Disassemble instructions
|
|||||||
|
|
||||||
### Symbols & Types
|
### Symbols & Types
|
||||||
```bash
|
```bash
|
||||||
ghidra symbol list # List symbols
|
ghidra symbol list # List symbols
|
||||||
ghidra symbol create <name> <addr> # Create symbol
|
ghidra symbol create <addr> <name> # Create symbol
|
||||||
ghidra symbol rename <old> <new> # Rename symbol
|
ghidra symbol rename <old> <new> # Rename symbol
|
||||||
ghidra type list # List data types
|
ghidra type list # List data types
|
||||||
ghidra type get <name> # Get type details
|
ghidra type get <name> # Get type details
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cross-References
|
### Cross-References
|
||||||
@@ -125,27 +125,31 @@ ghidra find crypto # Find crypto constants
|
|||||||
ghidra find interesting # Find interesting patterns
|
ghidra find interesting # Find interesting patterns
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call Graphs
|
### Call Graphs
|
||||||
```bash
|
```bash
|
||||||
ghidra graph calls <func> # Full call graph
|
ghidra graph calls # Full call graph
|
||||||
ghidra graph callers <func> # Who calls this?
|
ghidra graph callers <func> # Who calls this? (--depth optional)
|
||||||
ghidra graph callees <func> # What does this call?
|
ghidra graph callees <func> # What does this call? (--depth optional)
|
||||||
ghidra graph export dot # Export to DOT format
|
ghidra graph export dot # Export to DOT format
|
||||||
```
|
```
|
||||||
|
|
||||||
### Binary Patching
|
### Binary Patching
|
||||||
```bash
|
```bash
|
||||||
ghidra patch bytes <addr> "90 90" # Patch bytes
|
ghidra patch bytes <addr> "90 90" # Patch bytes
|
||||||
ghidra patch nop <addr> --count 5 # NOP out instructions
|
ghidra patch nop <addr> --count 5 # NOP out instructions
|
||||||
ghidra patch export # Export as patch file
|
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
|
### Comments
|
||||||
```bash
|
```bash
|
||||||
ghidra comment get <address> # Get comment
|
ghidra comment get <address> # Get comment
|
||||||
ghidra comment set <addr> "note" # Set comment
|
ghidra comment set <addr> "note" --comment-type EOL # Set comment
|
||||||
ghidra comment list # List all comments
|
ghidra comment list # List all comments
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: `--comment-type` currently falls back to `EOL` due client/bridge argument key mismatch.
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
```bash
|
```bash
|
||||||
@@ -247,8 +251,8 @@ Example workflow with an AI agent:
|
|||||||
2. `ghidra find interesting` - AI analyzes suspicious patterns
|
2. `ghidra find interesting` - AI analyzes suspicious patterns
|
||||||
3. `ghidra decompile <func>` - AI examines specific functions
|
3. `ghidra decompile <func>` - AI examines specific functions
|
||||||
4. `ghidra x-ref to <addr>` - AI traces data flow
|
4. `ghidra x-ref to <addr>` - AI traces data flow
|
||||||
5. `ghidra patch nop <addr>` - AI patches anti-debug code
|
5. `ghidra patch nop <addr>` - AI patches anti-debug code
|
||||||
6. `ghidra patch export` - Export patched binary
|
6. `ghidra patch export -o patched.bin` - Export patched binary
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Plan: Complete All Stub Commands
|
# 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# E2E Test Coverage Plan
|
# 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Ghidra-CLI Open Source Release Plan (Daemon-Only Architecture)
|
# 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
|
## 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.
|
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.
|
||||||
|
|||||||
+117
-7
@@ -312,12 +312,25 @@ pub enum FunctionCommands {
|
|||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct FunctionGetArgs {
|
pub struct FunctionGetArgs {
|
||||||
/// Function address or name
|
/// Function target (name/address/FUN_...)
|
||||||
pub target: String,
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct RenameArgs {
|
pub struct RenameArgs {
|
||||||
pub old_name: String,
|
pub old_name: String,
|
||||||
@@ -436,11 +449,25 @@ pub enum XRefCommands {
|
|||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct XRefArgs {
|
pub struct XRefArgs {
|
||||||
pub address: String,
|
/// XRef target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// XRef target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub enum TypeCommands {
|
pub enum TypeCommands {
|
||||||
/// List data types
|
/// List data types
|
||||||
@@ -555,11 +582,25 @@ pub struct FindFunctionArgs {
|
|||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct FindCallsArgs {
|
pub struct FindCallsArgs {
|
||||||
pub function: String,
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub enum GraphCommands {
|
pub enum GraphCommands {
|
||||||
/// Call graph
|
/// Call graph
|
||||||
@@ -576,13 +617,27 @@ pub enum GraphCommands {
|
|||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct GraphFunctionArgs {
|
pub struct GraphFunctionArgs {
|
||||||
pub function: String,
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub depth: Option<usize>,
|
pub depth: Option<usize>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct GraphExportArgs {
|
pub struct GraphExportArgs {
|
||||||
/// Export format (e.g., dot, json)
|
/// Export format (e.g., dot, json)
|
||||||
@@ -594,14 +649,33 @@ pub struct GraphExportArgs {
|
|||||||
|
|
||||||
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct DecompileArgs {
|
pub struct DecompileArgs {
|
||||||
pub target: String,
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// Function target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Args, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct DisasmArgs {
|
pub struct DisasmArgs {
|
||||||
pub address: String,
|
/// Disassembly target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(value_name = "TARGET", required_unless_present = "target")]
|
||||||
|
pub positional_target: Option<String>,
|
||||||
|
/// Disassembly target (name | 0xaddr | FUN_<hex>)
|
||||||
|
#[arg(long = "target", value_name = "TARGET")]
|
||||||
|
pub target: Option<String>,
|
||||||
/// Number of instructions to disassemble
|
/// Number of instructions to disassemble
|
||||||
#[arg(long = "instructions", short = 'n')]
|
#[arg(long = "instructions", short = 'n')]
|
||||||
pub num_instructions: Option<usize>,
|
pub num_instructions: Option<usize>,
|
||||||
@@ -609,6 +683,15 @@ pub struct DisasmArgs {
|
|||||||
pub options: QueryOptions,
|
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)]
|
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub enum DiffCommands {
|
pub enum DiffCommands {
|
||||||
/// Compare two programs
|
/// Compare two programs
|
||||||
@@ -838,3 +921,30 @@ pub struct SetupArgs {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub force: bool,
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import ghidra.util.task.TaskMonitor;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -188,6 +189,10 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
case "ping": return handlePing();
|
case "ping": return handlePing();
|
||||||
case "program_info": return handleProgramInfo();
|
case "program_info": return handleProgramInfo();
|
||||||
case "list_functions": return handleListFunctions(args);
|
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 "decompile": return handleDecompile(args);
|
||||||
case "list_strings": return handleListStrings(args);
|
case "list_strings": return handleListStrings(args);
|
||||||
case "list_imports": return handleListImports();
|
case "list_imports": return handleListImports();
|
||||||
@@ -479,6 +484,242 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
return result;
|
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<String> containsMatches = new ArrayList<>();
|
||||||
|
List<String> 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<String> 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_<hex>.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private JsonObject handleDecompile(JsonObject args) {
|
||||||
if (currentProgram == null) {
|
if (currentProgram == null) {
|
||||||
return errorResult("No program loaded");
|
return errorResult("No program loaded");
|
||||||
@@ -491,7 +732,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
|
|
||||||
Address addr = resolveAddress(addrStr);
|
Address addr = resolveAddress(addrStr);
|
||||||
if (addr == null) {
|
if (addr == null) {
|
||||||
return errorResult("Cannot resolve address or function name: " + addrStr);
|
return errorResult(buildFunctionTargetHint(addrStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
FunctionManager fm = currentProgram.getFunctionManager();
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
@@ -672,7 +913,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
|
|
||||||
Address addr = resolveAddress(addrStr);
|
Address addr = resolveAddress(addrStr);
|
||||||
if (addr == null) {
|
if (addr == null) {
|
||||||
return errorResult("Cannot resolve address or function name: " + addrStr);
|
return errorResult(buildFunctionTargetHint(addrStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonArray xrefs = new JsonArray();
|
JsonArray xrefs = new JsonArray();
|
||||||
@@ -719,7 +960,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
|
|
||||||
Address addr = resolveAddress(addrStr);
|
Address addr = resolveAddress(addrStr);
|
||||||
if (addr == null) {
|
if (addr == null) {
|
||||||
return errorResult("Cannot resolve address or function name: " + addrStr);
|
return errorResult(buildFunctionTargetHint(addrStr));
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonArray xrefs = new JsonArray();
|
JsonArray xrefs = new JsonArray();
|
||||||
@@ -1310,7 +1551,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
Function targetFunc = findFunctionByNameOrAddress(functionTarget);
|
Function targetFunc = findFunctionByNameOrAddress(functionTarget);
|
||||||
|
|
||||||
if (targetFunc == null) {
|
if (targetFunc == null) {
|
||||||
return errorResult("Function not found: " + functionTarget);
|
return errorResult(buildFunctionTargetHint(functionTarget));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
||||||
@@ -2100,7 +2341,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
int depth = getArgInt(args, "depth", 1);
|
int depth = getArgInt(args, "depth", 1);
|
||||||
|
|
||||||
Function targetFunc = findFunctionByNameOrAddress(funcName);
|
Function targetFunc = findFunctionByNameOrAddress(funcName);
|
||||||
if (targetFunc == null) return errorResult("Function not found: " + funcName);
|
if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName));
|
||||||
|
|
||||||
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
||||||
FunctionManager fm = currentProgram.getFunctionManager();
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
@@ -2151,7 +2392,7 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
int depth = getArgInt(args, "depth", 1);
|
int depth = getArgInt(args, "depth", 1);
|
||||||
|
|
||||||
Function targetFunc = findFunctionByNameOrAddress(funcName);
|
Function targetFunc = findFunctionByNameOrAddress(funcName);
|
||||||
if (targetFunc == null) return errorResult("Function not found: " + funcName);
|
if (targetFunc == null) return errorResult(buildFunctionTargetHint(funcName));
|
||||||
|
|
||||||
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
ReferenceManager refMgr = currentProgram.getReferenceManager();
|
||||||
FunctionManager fm = currentProgram.getFunctionManager();
|
FunctionManager fm = currentProgram.getFunctionManager();
|
||||||
@@ -2300,8 +2541,8 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
Function func1 = findFunctionByNameOrAddress(func1Target);
|
Function func1 = findFunctionByNameOrAddress(func1Target);
|
||||||
Function func2 = findFunctionByNameOrAddress(func2Target);
|
Function func2 = findFunctionByNameOrAddress(func2Target);
|
||||||
|
|
||||||
if (func1 == null) return errorResult("Function not found: " + func1Target);
|
if (func1 == null) return errorResult(buildFunctionTargetHint(func1Target));
|
||||||
if (func2 == null) return errorResult("Function not found: " + func2Target);
|
if (func2 == null) return errorResult(buildFunctionTargetHint(func2Target));
|
||||||
|
|
||||||
DecompInterface decompiler = new DecompInterface();
|
DecompInterface decompiler = new DecompInterface();
|
||||||
try {
|
try {
|
||||||
|
|||||||
+23
-14
@@ -628,20 +628,25 @@ fn execute_via_bridge(
|
|||||||
"memory" => client.memory_map(),
|
"memory" => client.memory_map(),
|
||||||
other => anyhow::bail!("Query type '{}' not supported", other),
|
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) => {
|
Commands::Function(cmd) => {
|
||||||
use cli::FunctionCommands;
|
use cli::FunctionCommands;
|
||||||
match cmd {
|
match cmd {
|
||||||
FunctionCommands::List(opts) => {
|
FunctionCommands::List(opts) => {
|
||||||
client.list_functions(opts.limit.or(default_limit), opts.filter.clone())
|
client.list_functions(opts.limit.or(default_limit), opts.filter.clone())
|
||||||
}
|
}
|
||||||
FunctionCommands::Decompile(args) => client.decompile(args.target.clone()),
|
FunctionCommands::Decompile(args) => {
|
||||||
FunctionCommands::Get(args) => {
|
client.decompile(args.resolved_target().to_string())
|
||||||
client.send_command("get_function", Some(json!({"address": args.target})))
|
|
||||||
}
|
}
|
||||||
FunctionCommands::Disasm(args) => client.disasm(&args.target, None),
|
FunctionCommands::Get(args) => {
|
||||||
FunctionCommands::Calls(args) => client.find_calls(&args.target),
|
client.send_command(
|
||||||
FunctionCommands::XRefs(args) => client.xrefs_to(args.target.clone()),
|
"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(
|
FunctionCommands::Rename(args) => client.send_command(
|
||||||
"rename_function",
|
"rename_function",
|
||||||
Some(json!({
|
Some(json!({
|
||||||
@@ -659,7 +664,7 @@ fn execute_via_bridge(
|
|||||||
FunctionCommands::Delete(args) => client.send_command(
|
FunctionCommands::Delete(args) => client.send_command(
|
||||||
"delete_function",
|
"delete_function",
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"address": args.target,
|
"address": args.resolved_target(),
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -712,8 +717,8 @@ fn execute_via_bridge(
|
|||||||
Commands::XRef(cmd) => {
|
Commands::XRef(cmd) => {
|
||||||
use cli::XRefCommands;
|
use cli::XRefCommands;
|
||||||
match cmd {
|
match cmd {
|
||||||
XRefCommands::To(args) => client.xrefs_to(args.address.clone()),
|
XRefCommands::To(args) => client.xrefs_to(args.resolved_target().to_string()),
|
||||||
XRefCommands::From(args) => client.xrefs_from(args.address.clone()),
|
XRefCommands::From(args) => client.xrefs_from(args.resolved_target().to_string()),
|
||||||
XRefCommands::List(_) => client.send_command("xrefs_list", None),
|
XRefCommands::List(_) => client.send_command("xrefs_list", None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -777,8 +782,12 @@ fn execute_via_bridge(
|
|||||||
use cli::GraphCommands;
|
use cli::GraphCommands;
|
||||||
match cmd {
|
match cmd {
|
||||||
GraphCommands::Calls(opts) => client.graph_calls(opts.limit.or(default_limit)),
|
GraphCommands::Calls(opts) => client.graph_calls(opts.limit.or(default_limit)),
|
||||||
GraphCommands::Callers(args) => client.graph_callers(&args.function, args.depth),
|
GraphCommands::Callers(args) => {
|
||||||
GraphCommands::Callees(args) => client.graph_callees(&args.function, args.depth),
|
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),
|
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::String(args) => client.find_string(&args.pattern),
|
||||||
FindCommands::Bytes(args) => client.find_bytes(&args.hex),
|
FindCommands::Bytes(args) => client.find_bytes(&args.hex),
|
||||||
FindCommands::Function(args) => client.find_function(&args.pattern),
|
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::Crypto(_) => client.find_crypto(),
|
||||||
FindCommands::Interesting(_) => client.find_interesting(),
|
FindCommands::Interesting(_) => client.find_interesting(),
|
||||||
}
|
}
|
||||||
@@ -819,7 +828,7 @@ fn execute_via_bridge(
|
|||||||
ScriptCommands::List => client.script_list(),
|
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) => {
|
Commands::Batch(args) => {
|
||||||
// Read batch file and execute each command locally
|
// Read batch file and execute each command locally
|
||||||
let content = std::fs::read_to_string(&args.script_file)
|
let content = std::fs::read_to_string(&args.script_file)
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ cargo test --test command_tests test_version
|
|||||||
|
|
||||||
Run tests that don't need Ghidra:
|
Run tests that don't need Ghidra:
|
||||||
```bash
|
```bash
|
||||||
cargo test --test e2e --test command_tests --test output_format_integration
|
cargo test --test e2e --test output_format_integration
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test Requirements
|
## Test Requirements
|
||||||
|
|||||||
Reference in New Issue
Block a user