diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..37e37c2 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,259 @@ +--- +name: ghidra-cli-skill +description: > + Use ghidra-cli for reverse engineering tasks: binary analysis, decompilation, function inspection, cross-reference analysis, pattern discovery, and binary patching. + Activate when the user requests: + - Binary analysis or reverse engineering + - Decompilation or disassembly + - Function listing, inspection, or renaming + - Cross-reference or call graph analysis + - String or byte pattern searches + - Binary patching or modification + - Ghidra project management +--- + +# ghidra-cli + +Use ghidra-cli for reverse engineering tasks: binary analysis, decompilation, function inspection, cross-reference analysis, pattern discovery, and binary patching. + +## When to Use + +Activate when the user requests: +- Binary analysis or reverse engineering +- Decompilation or disassembly +- Function listing, inspection, or renaming +- Cross-reference or call graph analysis +- String or byte pattern searches +- Binary patching or modification +- Ghidra project management + +## Workflow + +### Pre-flight Check + +Before running queries, verify the environment: + +```bash +# Check if daemon is running for fast queries +ghidra daemon status --project + +# If not running, start it +ghidra daemon start --project --program +``` + +### Quick Start (New Binary) + +For one-off analysis, use quick mode: + +```bash +ghidra quick ./binary +ghidra daemon start --project quick-analysis --program binary +``` + +### Full Project Setup + +For sustained analysis: + +```bash +ghidra project create myproject +ghidra import ./binary --project myproject +ghidra analyze --project myproject --program binary +ghidra daemon start --project myproject --program binary +``` + +## Command Reference + +### Querying Functions + +```bash +# List all functions +ghidra function list --project

--program + +# Filter functions by size or name +ghidra function list --filter "size > 500" +ghidra function list --filter "name contains 'crypt'" + +# Get function details +ghidra function get main + +# Decompile to pseudocode +ghidra function decompile main + +# Disassemble +ghidra function disasm main + +# Cross-references +ghidra function xrefs main +ghidra function calls main +``` + +### Search Operations + +```bash +# Find functions by pattern +ghidra find function "*crypt*" + +# Find strings +ghidra find string "password" + +# Find byte patterns (hex) +ghidra find bytes "4883ec08" + +# Find crypto constants +ghidra find crypto + +# Find suspicious patterns (anti-analysis, obfuscation) +ghidra find interesting +``` + +### Cross-References + +```bash +# References TO an address +ghidra x-ref to 0x401000 + +# References FROM an address +ghidra x-ref from 0x401000 +``` + +### Call Graphs + +```bash +# Full call graph +ghidra graph calls + +# Who calls this function (callers) +ghidra graph callers main --depth 3 + +# What does this function call (callees) +ghidra graph callees main --depth 3 + +# Export as DOT format +ghidra graph export dot +``` + +### Symbols and Strings + +```bash +# List symbols +ghidra symbol list + +# List strings +ghidra strings list --limit 100 + +# References to a string +ghidra strings refs "error" +``` + +### Memory and Types + +```bash +# Memory map +ghidra memory map + +# Read memory at address +ghidra memory read 0x401000 64 + +# List data types +ghidra type list + +# Apply type to address +ghidra type apply 0x402000 "char[32]" +``` + +### Modifications + +```bash +# Rename function +ghidra function rename sub_401000 decrypt_password + +# Add comment +ghidra comment set 0x401000 "Key derivation starts here" + +# Patch bytes +ghidra patch bytes 0x401000 "90909090" + +# NOP instructions +ghidra patch nop 0x401010 --count 5 + +# Export patched binary +ghidra patch export --output patched.bin +``` + +### Scripting + +```bash +# Run Python script +ghidra script run analysis.py + +# Inline Python +ghidra script python "print(currentProgram.getName())" + +# Batch commands from file +ghidra batch commands.txt +``` + +## Output Handling + +ghidra-cli outputs JSON by default. Parse the structured data: + +```bash +# JSON output (default) +ghidra function list + +# Table format for display +ghidra function list --format table + +# Count only +ghidra function list --format count +``` + +When processing results, extract relevant fields from JSON rather than displaying raw output. + +## Common Patterns + +### Investigate a Function + +```bash +ghidra function get # Overview +ghidra function decompile # Pseudocode +ghidra function calls # What it calls +ghidra function xrefs # Who calls it +ghidra graph callers --depth 2 +``` + +### Find Interesting Code + +```bash +ghidra find crypto # Crypto constants +ghidra find interesting # Suspicious patterns +ghidra find function "*alloc*" # Memory functions +ghidra strings list --filter "length > 50" +``` + +### Trace Data Flow + +```bash +ghidra x-ref to

# Who writes here +ghidra x-ref from
# What this references +ghidra graph callees --depth 3 +``` + +## Error Recovery + +| Situation | Resolution | +| ------------------ | ------------------------------------------------------------- | +| Daemon not running | `ghidra daemon start --project

--program ` | +| No project exists | `ghidra project create ` or use `ghidra quick ` | +| Function not found | Use `ghidra find function "*pattern*"` to search | +| Address format | Use hex with 0x prefix: `0x401000` | +| Slow queries | Start daemon for sub-second response times | + +## Global Options + +All commands accept: +- `--project ` - Target project +- `--program ` - Target program within project +- `--format json|table|count` - Output format +- `--filter ` - Filter expression +- `--limit ` - Max results diff --git a/docs/plans/refactor-NOTES.md b/docs/plans/refactor-NOTES.md deleted file mode 100644 index 8bb9659..0000000 --- a/docs/plans/refactor-NOTES.md +++ /dev/null @@ -1,105 +0,0 @@ -# Refactoring Notes - -## Architecture Reference: debugger-cli - -The `debugger-cli` project at `~/git/debugger-cli` provides a good pattern for daemon-based CLIs: - -### Key Patterns Used - -1. **IPC via Local Sockets** (`src/ipc/`) - - Uses `interprocess` crate for cross-platform Unix sockets / Windows named pipes - - Length-prefixed JSON messages (4-byte little-endian length + payload) - - Separate `protocol.rs`, `transport.rs`, and `client.rs` modules - -2. **Daemon Architecture** (`src/daemon/`) - - `server.rs` - Main event loop with IPC listener - - `handler.rs` - Command routing and execution - - `session.rs` - State management for debug sessions - -3. **Clean Separation** - - IPC protocol defines its own `Command` enum (not reusing CLI args) - - Handler translates protocol commands to domain operations - - Session holds the actual debug adapter connection - ---- - -## Implementation Progress - -### Phase 1: Bridge Script ✅ -- Created `src/ghidra/scripts/bridge.py` - persistent TCP server inside Ghidra -- Implements handlers: `ping`, `program_info`, `list_functions`, `decompile`, `list_strings`, `list_imports`, `list_exports`, `memory_map`, `xrefs_to`, `xrefs_from` -- Uses `---GHIDRA_CLI_START---` / `---GHIDRA_CLI_END---` markers for ready signal - -### Phase 2: Output Markers ✅ -- Updated all 8 Python scripts in `scripts.rs` with delimiters -- Updated `headless.rs` to use marker-based extraction instead of fragile brace-counting - -### Phase 3: IPC Layer ✅ -- Added `interprocess` crate to `Cargo.toml` -- Created `src/ipc/mod.rs` with: - - `protocol.rs` - Typed `Command` enum, `Request`, `Response` structures - - `transport.rs` - Cross-platform socket wrapper with length-prefixed framing - - `client.rs` - `DaemonClient` for CLI-to-daemon communication - -### Phase 4: Bridge Manager ✅ -- Created `src/ghidra/bridge.rs` with `GhidraBridge` struct -- Manages Ghidra process lifecycle (spawn, monitor, shutdown) -- TCP connection to Python bridge script -- `BridgeResponse` typed response handling -- Embeds bridge.py via `include_str!` macro - -### Phase 5: Daemon Update ✅ -- Created `src/daemon/handler.rs` - routes IPC commands to bridge -- Created `src/daemon/ipc_server.rs` - local socket IPC server -- Refactored `src/daemon/mod.rs` to manage `GhidraBridge` and IPC server -- Daemon now starts both IPC server (port 18701) and legacy TCP RPC - -### Phase 6: Typed Responses ✅ -- `BridgeResponse` created in `bridge.rs` for typed deserialization -- IPC `Response` uses `serde_json::Value` for flexibility -- Handler deserializes bridge responses into typed structures - -### Phase 7: GUI Integration (Optional) -- Status: Not started -- Future work: `goto`, `highlight` commands - ---- - -## Files Created/Modified - -### New Files -- `src/ghidra/scripts/bridge.py` - Persistent Python bridge server -- `src/ghidra/bridge.rs` - Rust bridge manager -- `src/ipc/mod.rs` - IPC module root -- `src/ipc/protocol.rs` - Typed protocol definitions -- `src/ipc/transport.rs` - Socket transport layer -- `src/ipc/client.rs` - Daemon client - -- `src/daemon/handler.rs` - IPC command handler -- `src/daemon/ipc_server.rs` - Local socket IPC server - -### Modified Files -- `Cargo.toml` - Added `interprocess` crate -- `src/main.rs` - Added `mod ipc`, updated daemon config -- `src/ghidra/mod.rs` - Added `mod bridge`, `#[derive(Debug)]` on `GhidraClient` -- `src/ghidra/scripts.rs` - All scripts now have output markers -- `src/ghidra/headless.rs` - Marker-based JSON extraction -- `src/daemon/mod.rs` - Integrated bridge and IPC server - ---- - -## Remaining Work - -1. **Manual testing** - Test with actual Ghidra installation -2. **GUI Integration (Phase 7)** - Optional `goto`, `highlight` commands -3. **Cleanup** - Remove unused transport functions, fix warnings - ---- - -## Build Status - -``` -✅ cargo build --release - PASSED -✅ cargo test - 30 passed, 1 pre-existing failure (test_parse_hex) -⚠️ 48 warnings (mostly unused code, can be cleaned up) -``` diff --git a/docs/plans/refactor.md b/docs/plans/refactor.md deleted file mode 100644 index fd48caf..0000000 --- a/docs/plans/refactor.md +++ /dev/null @@ -1,232 +0,0 @@ - -Based on the code you uploaded, the current architecture is a **"One-Shot Headless"** model. Every time a query runs, the CLI (or Daemon) spawns a new `analyzeHeadless` process, initializes the JVM, loads the project, runs a script, and shuts down. - -This is robust in terms of isolation (if it crashes, it doesn't affect the next run), but it is **slow** (high latency due to JVM startup) and **fragile** (parsing `stdout` mixed with Ghidra logs). - -Here is a roadmap to make the CLI significantly more robust and deeply integrated with the IDE. - ---- - -### Part 1: Improving Robustness (The "Persistent Bridge" Architecture) - -To make this robust, you need to move from "Spawn Process -> Parse Stdout" to "Spawn Process -> Connect via Socket -> Keep Alive". This prevents the overhead of restarting Ghidra for every command. - -#### 1. Create a "Ghidra Bridge" Python Script - -Instead of many small scripts (`get_list_functions.py`, etc.), create one master Python script that runs an infinite loop inside Ghidra (Headless or GUI) and listens for JSON commands. - -**File:** `src/ghidra/scripts/bridge.py` (New file) - -```python -# @category Bridge -# @keybinding -# @menupath Tools.Start CLI Bridge -# @toolbar - -import socket -import json -import threading -from ghidra.util.task import ConsoleTaskMonitor -from ghidra.app.decompiler import DecompInterface - -# Define your command handlers here -def handle_functions(args): - # ... (Logic from your existing get_list_functions_script) - return [{"name": "example", "addr": "0x1234"}] - -def handle_decompile(args): - # ... (Logic from your existing decompile script) - return {"code": "int main() { ... }"} - -COMMANDS = { - "functions": handle_functions, - "decompile": handle_decompile -} - -def start_server(port=12345): - 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) - print(json.dumps({"status": "ready", "port": port})) - - while True: - conn, addr = s.accept() - try: - # Read line-based JSON - f = conn.makefile() - while True: - line = f.readline() - if not line: break - - try: - req = json.loads(line) - cmd = req.get("command") - args = req.get("args", {}) - - if cmd in COMMANDS: - result = COMMANDS[cmd](args) - response = {"status": "success", "data": result} - else: - response = {"status": "error", "message": "Unknown command"} - - conn.sendall(json.dumps(response) + "\n") - except Exception as e: - conn.sendall(json.dumps({"status": "error", "message": str(e)}) + "\n") - except: - pass - finally: - conn.close() - -if __name__ == "__main__": - # If running in GUI, run in background thread to not freeze UI - if isRunningHeadless(): - start_server() - else: - t = threading.Thread(target=start_server) - t.start() - -``` - -#### 2. Update `HeadlessExecutor` to manage a Lifecycle - -Modify `src/ghidra/headless.rs` or `src/daemon/process.rs`. Instead of just `Command::new()`, you need a struct that holds the `Child` process and a `TcpStream`. - -* **Startup:** Spawn `analyzeHeadless` with the `bridge.py` script. -* **Handshake:** Wait for the specific JSON `{"status": "ready"}` on stdout. -* **Execution:** Connect to the port via TCP. Send requests as JSON lines. -* **Cleanup:** Kill the child process on daemon shutdown. - -This eliminates `stdout` parsing issues because data transfer happens over a clean TCP socket. - ---- - -### Part 2: Better IDE Integration (Bi-directional Control) - -Currently, your CLI talks to a headless instance. The user wants to see results in the GUI. - -#### 1. Shared Project Locking - -Ghidra does not allow a Headless instance and a GUI instance to have write access to the same project simultaneously. - -* **Robustness Fix:** The CLI should detect if the GUI is open. -* **Strategy:** If the GUI is open, the CLI should **not** spawn a headless instance. Instead, it should connect to the *Bridge* running inside the GUI. - -#### 2. Context Synchronization (CLI -> GUI) - -Add commands to the Bridge script that manipulate the GUI state. - -**Update `bridge.py`:** - -```python -def handle_goto(args): - addr_str = args.get("address") - addr = currentProgram.getAddressFactory().getAddress(addr_str) - - # Check if we are in GUI mode - if not isRunningHeadless(): - from ghidra.framework.plugintool import PluginTool - state = state # Ghidra injects 'state' - tool = state.getTool() - if tool: - tool.firePluginEvent(...) # Or simpler: - # This often requires the script to be run via the Ghidra Script Manager - currentLocation = ProgramLocation(currentProgram, addr) - tool.setGoTo(currentLocation) - return {"status": "moved"} - -def handle_highlight(args): - # Set background color of address range - if not isRunningHeadless(): - setBackgroundColor(addr, Color.RED) - -``` - -**Update Rust CLI (`src/cli.rs`):** -Add a command `ghidra focus

` which sends the `goto` command to the bridge. - -#### 3. Automatic Discovery - -How does the CLI know if the GUI is running? - -1. **Port Scanning:** The Rust CLI can try to connect to the default Bridge port (e.g., 12345). -2. **Logic:** -* Try `TcpStream::connect("127.0.0.1:12345")`. -* If successful -> **GUI Mode**. Send commands there. -* If failed -> **Headless Mode**. Check if Daemon is running. If not, start Daemon (which spawns Headless Bridge). - - - ---- - -### Part 3: Robustness Improvements in Rust - -#### 1. Fix Output Parsing (`src/ghidra/headless.rs`) - -Your current `extract_json_from_output` relies on counting braces. This is risky if the program being analyzed contains strings with braces. - -**Improved approach (if not using Bridge):** -Wrap the output in a unique delimiter in the Python script. - -*Python Script:* - -```python -print("---GHIDRA_CLI_START---") -print(json.dumps(data)) -print("---GHIDRA_CLI_END---") - -``` - -*Rust (`headless.rs`):* - -```rust -fn extract_json_from_output(&self, output: &str) -> Result { - let start_marker = "---GHIDRA_CLI_START---"; - let end_marker = "---GHIDRA_CLI_END---"; - - let start = output.find(start_marker) - .ok_or(GhidraError::ExecutionFailed("Missing start marker".into()))? - + start_marker.len(); - - let end = output.find(end_marker) - .ok_or(GhidraError::ExecutionFailed("Missing end marker".into()))?; - - Ok(output[start..end].trim().to_string()) -} - -``` - -#### 2. Typed Responses with `serde` - -In `src/ghidra/data.rs`, strictly enforce optionals. If Ghidra scripts fail (e.g., decompilation error), they should return a standard error object. - -```rust -#[derive(Deserialize)] -#[serde(tag = "status")] -enum BridgeResponse { - #[serde(rename = "success")] - Success { data: T }, - #[serde(rename = "error")] - Error { message: String }, -} - -``` - -Update `src/daemon/queue.rs` to parse this wrapper before returning the inner string. - ---- - -### Part 4: Implementation Plan - -Here is the recommended order of operations to upgrade your tool: - -1. **Implement the Bridge Script:** Create `scripts/ghidra_bridge.py`. -2. **Update Daemon to support "Long-Running" Process:** -* Modify `DaemonState` to hold a `Child` process handle. -* Modify `HeadlessExecutor` to check if the bridge is up; if so, use TCP; if not, spawn it. - - -3. **Add GUI Commands:** Add `goto`, `highlight`, and `select` to the bridge and `src/cli.rs`. -4. **Integration Test:** Open Ghidra GUI, run the bridge script manually. Then run `ghidra query functions` from your terminal. It should return results instantly using the GUI's memory. - -This transforms your tool from a "Batch Processor" to a "Live Assistant." \ No newline at end of file diff --git a/docs/plans/setup-command-NOTES.md b/docs/plans/setup-command-NOTES.md deleted file mode 100644 index 78192c2..0000000 --- a/docs/plans/setup-command-NOTES.md +++ /dev/null @@ -1,83 +0,0 @@ -# Setup Command Implementation Notes - -## Codebase Analysis - -### Current Structure -- **Cargo.toml**: Already has tokio, clap, serde, dirs, anyhow dependencies -- **src/cli.rs**: Commands enum on line 20-127, need to add Setup variant -- **src/main.rs**: - - `run()` sync function handles most commands (line 49-72) - - `run_async()` handles daemon commands (line 74-80) - - `run_with_daemon_check()` routes commands through daemon if running (line 82-125) -- **src/ghidra/mod.rs**: GhidraClient with `verify_installation()` - can reuse for verification -- **src/config.rs**: Config struct with `save()` method and `ghidra_install_dir` field - -### Key Insights -1. Setup command should be treated as async like daemon commands (uses reqwest for HTTP) -2. Need to route `Commands::Setup` through `run_async()` rather than sync `run()` -3. Can reuse existing `Config::save()` to persist ghidra_install_dir after installation -4. Can reuse `GhidraClient::verify_installation()` to verify the installation - -### Dependencies Added -```toml -reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"] } -zip = "0.6" -futures-util = "0.3" -indicatif = "0.17" -``` - -## Implementation Progress - -### Phase 1: Dependencies ✅ -Added reqwest, zip, futures-util, and indicatif to Cargo.toml - -### Phase 2: CLI Definition ✅ -- Added `Setup(SetupArgs)` variant to Commands enum -- Added `SetupArgs` struct with version, dir, and force fields - -### Phase 3: Setup Module ✅ -Created `src/ghidra/setup.rs` with: -- `check_java_requirement()` - runs `java -version` and checks for JDK 17+ -- `resolve_version_url()` - queries GitHub API for release URL -- `download_file()` - streams download with indicatif progress bar -- `extract_zip()` - extracts with progress bar, handles Unix permissions -- `install_ghidra()` - orchestrates the full installation flow - -### Phase 4: Main Integration ✅ -- Updated imports to include SetupArgs -- Modified main() to route Setup through run_async() -- Updated run_async() to handle Commands::Setup -- Added handle_setup() async function - -## Testing Notes - -### Build Verification -``` -cargo build # SUCCESS - only lint warnings -``` - -### Help Output -``` -$ ghidra setup --help -Download and setup Ghidra automatically - -Usage: ghidra setup [OPTIONS] - -Options: - --version Specific Ghidra version to install (e.g., "11.0"). Defaults to latest - -d, --dir Installation directory. Defaults to standard data directory - --force Skip Java check - -v, --verbose Enable verbose output - -q, --quiet Suppress non-essential output - -h, --help Print help -``` - -### Tests -All existing tests pass (cargo test). - -## Files Modified -- `Cargo.toml` - Added 4 new dependencies -- `src/cli.rs` - Added SetupArgs struct and Setup variant -- `src/ghidra/mod.rs` - Added `pub mod setup;` -- `src/ghidra/setup.rs` - NEW FILE - 230 lines -- `src/main.rs` - Updated routing and added handle_setup function diff --git a/docs/plans/setup-command.md b/docs/plans/setup-command.md deleted file mode 100644 index 225ead9..0000000 --- a/docs/plans/setup-command.md +++ /dev/null @@ -1,248 +0,0 @@ - -Here is the implementation plan to add a `ghidra setup` command that automates the downloading and installation of Ghidra. - -### 1. Update Dependencies - -First, we need to add crates for HTTP requests, file downloading, zip extraction, and progress bars. - -**Action:** Update `Cargo.toml` - -```toml -[dependencies] -# ... existing dependencies ... -reqwest = { version = "0.11", features = ["json", "stream", "rustls-tls"] } -zip = "0.6" -futures-util = "0.3" # For handling download streams -indicatif = "0.17" # For progress bars - -``` - -### 2. Update CLI Definition - -Add the `setup` command to the argument parser. - -**Action:** Modify `src/cli.rs` - -```rust -// In enum Commands -#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)] -pub enum Commands { - // ... existing commands ... - - /// Download and setup Ghidra automatically - Setup(SetupArgs), -} - -// Define arguments -#[derive(Args, Clone, Serialize, Deserialize, Debug)] -pub struct SetupArgs { - /// Specific Ghidra version to install (e.g., "11.0"). Defaults to latest. - #[arg(long)] - pub version: Option, - - /// Installation directory. Defaults to standard data directory. - #[arg(long, short = 'd')] - pub dir: Option, - - /// Skip Java check - #[arg(long)] - pub force: bool, -} - -``` - -### 3. Create Setup Module - -Create a new module to handle the download and extraction logic. This keeps `main.rs` clean. - -**Action:** Create `src/ghidra/setup.rs` - -This file will contain logic to: - -1. **Check for Java**: Run `java -version` to ensure prerequisites are met. -2. **Fetch Release Info**: Query the GitHub API (`https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/latest`) to get the download URL. -3. **Download**: Stream the zip file with a progress bar using `reqwest` and `indicatif`. -4. **Extract**: Unzip the file to the target directory using `zip`. -5. **Detect Installation**: Find the actual Ghidra folder inside the zip (usually `ghidra_X.X.X_PUBLIC`). - -**Sketch of `src/ghidra/setup.rs`:** - -```rust -use std::path::{Path, PathBuf}; -use std::fs::File; -use std::io::Write; -use anyhow::{Context, Result, anyhow}; -use futures_util::StreamExt; -use indicatif::{ProgressBar, ProgressStyle}; - -pub async fn install_ghidra(version: Option, target_dir: PathBuf) -> Result { - // 1. Resolve Version & URL (GitHub API or hardcoded fallback for specific versions) - let (download_url, filename) = resolve_version_url(version).await?; - - // 2. Download File - let zip_path = target_dir.join(&filename); - download_file(&download_url, &zip_path).await?; - - // 3. Extract - let install_path = extract_zip(&zip_path, &target_dir)?; - - // 4. Cleanup zip - std::fs::remove_file(zip_path)?; - - Ok(install_path) -} - -async fn download_file(url: &str, path: &Path) -> Result<()> { - let client = reqwest::Client::new(); - let res = client.get(url).send().await?; - let total_size = res.content_length().unwrap_or(0); - - let pb = ProgressBar::new(total_size); - pb.set_style(ProgressStyle::default_bar() - .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")? - .progress_chars("#>-")); - pb.set_message(format!("Downloading from {}", url)); - - let mut file = File::create(path)?; - let mut stream = res.bytes_stream(); - - while let Some(item) = stream.next().await { - let chunk = item?; - file.write_all(&chunk)?; - pb.inc(chunk.len() as u64); - } - pb.finish_with_message("Download complete"); - Ok(()) -} - -fn extract_zip(zip_path: &Path, target_dir: &Path) -> Result { - // Uses 'zip' crate to extract - // Returns the path to the extracted 'ghidra_X.X.X' directory -} - -pub fn check_java_requirement() -> Result<()> { - // Exec "java -version" and check output -} - -``` - -### 4. Integrate Module - -Expose the new module. - -**Action:** Modify `src/ghidra/mod.rs` - -```rust -pub mod setup; -// ... existing code ... - -``` - -### 5. Implement Handler in Main - -Connect the CLI command to the logic and update the configuration upon success. - -**Action:** Modify `src/main.rs` - -1. Add to the `run` match arm: -```rust -// In run() function -Commands::Setup(args) => handle_setup(args).await, - -``` - - -2. Implement `handle_setup`: -```rust -async fn handle_setup(args: cli::SetupArgs) -> anyhow::Result<()> { - println!("Ghidra Setup Wizard"); - println!("==================="); - - // 1. Check Java - if !args.force { - if let Err(e) = ghidra::setup::check_java_requirement() { - eprintln!("Warning: Java prerequisite check failed: {}", e); - eprintln!("Ghidra requires JDK 17+. Continue anyway? [y/N]"); - // ... input confirmation logic ... - } - } - - // 2. Determine Install Directory - let install_base = if let Some(d) = args.dir { - PathBuf::from(d) - } else { - // Default to XDG_DATA_HOME/ghidra-cli/ghidra - 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 - println!("Installing 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..."); - // Reuse existing doctor logic or verify_installation() - let client = GhidraClient::new(config)?; - client.verify_installation()?; - println!("Verification passed!"); - - Ok(()) -} - -``` - - - -### 6. Make Main Async-Aware for Sync Commands - -Currently `run` is synchronous, but `reqwest` is async. - -* The `main` function is already `#[tokio::main]`. -* We need to change `run(cli: Cli)` to `async fn run(cli: Cli)`. -* Most existing handlers in `run` are synchronous; calling them from an async function is fine. -* However, `handle_setup` needs to be awaited. - -**Refactoring:** -Change the signature of `run` in `src/main.rs`: - -```rust -async fn run(cli: Cli) -> anyhow::Result<()> { - match cli.command { - // ... - Commands::Setup(args) => handle_setup(args).await, // New async handler - _ => { - // Existing sync handlers can wrap in simple blocks if needed, - // or just be called directly as they return Result - match cli.command { - Commands::Query(args) => handle_query(args), - // ... rest of sync commands - _ => Ok(()) - } - } - } -} - -``` - -### Summary of Workflow - -1. User runs `ghidra setup`. -2. CLI checks for Java. -3. CLI fetches latest release URL from GitHub. -4. CLI downloads ~300MB+ zip file showing progress. -5. CLI unzips it. -6. CLI updates `config.yaml` automatically setting `ghidra_install_dir`. -7. User can immediately run `ghidra quick binary.exe`.