mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
changes to daemon mode
This commit is contained in:
+378
-100
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,49 @@
|
||||
NEVER SKIP TESTS! IF GHIDRA IS NOT INSTALLED, THE TESTS MUST FAIL.
|
||||
# Agent Instructions
|
||||
|
||||
DEFAULT OUTPUT FORMAT SHOULD BE HUMAN AND AGENT READABLE, NOT JSON (--json and --pretty for json/pretty json)
|
||||
## Critical Rules
|
||||
|
||||
1. **NEVER SKIP TESTS!** If Ghidra is not installed, the tests MUST fail.
|
||||
2. **DEFAULT OUTPUT FORMAT** should be human and agent readable, NOT JSON. Use `--json` and `--pretty` for JSON output.
|
||||
|
||||
## Architecture
|
||||
|
||||
ghidra-cli uses a **daemon-only architecture**:
|
||||
- All commands route through a daemon process
|
||||
- Daemon manages a persistent Ghidra bridge connection
|
||||
- Import/Analyze/Quick commands auto-start the daemon
|
||||
- One daemon per project, one program per daemon
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Starting Analysis
|
||||
```bash
|
||||
# Quickest path - auto-starts daemon
|
||||
ghidra quick ./binary
|
||||
|
||||
# Or explicit steps (daemon auto-starts on import)
|
||||
ghidra import ./binary --project myproj --program prog
|
||||
ghidra analyze --project myproj --program prog
|
||||
```
|
||||
|
||||
### Daemon is Always Running
|
||||
After import/analyze/quick, the daemon is running. All query commands use it automatically:
|
||||
```bash
|
||||
ghidra function list # Uses daemon
|
||||
ghidra decompile main # Uses daemon
|
||||
ghidra find crypto # Uses daemon
|
||||
```
|
||||
|
||||
### Manual Daemon Control
|
||||
```bash
|
||||
ghidra daemon status # Check if running
|
||||
ghidra daemon stop # Stop daemon
|
||||
ghidra daemon restart --project p --program new_prog # Switch program
|
||||
```
|
||||
|
||||
## Code Organization
|
||||
|
||||
- `src/main.rs` - CLI entry point, command routing
|
||||
- `src/daemon/` - Daemon process, IPC server, command handlers
|
||||
- `src/ghidra/bridge.rs` - Ghidra bridge connection management
|
||||
- `src/ghidra/scripts/bridge.py` - Python script running inside Ghidra
|
||||
- `src/ipc/` - IPC protocol and client
|
||||
|
||||
@@ -4,16 +4,38 @@ A high-performance Rust CLI for automating Ghidra reverse engineering tasks, des
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast daemon mode** - Keeps Ghidra loaded in memory for sub-second response times
|
||||
- **Daemon-only architecture** - All operations route through a persistent daemon for consistency
|
||||
- **Auto-start daemon** - Import/analyze commands automatically start the daemon
|
||||
- **Fast queries** - Sub-second response times with Ghidra kept in memory
|
||||
- **Comprehensive analysis** - Functions, symbols, types, strings, cross-references
|
||||
- **Binary patching** - Modify bytes, NOP instructions, export patches
|
||||
- **Call graphs** - Generate caller/callee graphs, export to DOT format
|
||||
- **Search capabilities** - Find strings, bytes, functions, crypto patterns
|
||||
- **Script execution** - Run Python/Java scripts, inline or from files
|
||||
- **Batch operations** - Execute multiple commands from a file
|
||||
- **Flexible output** - JSON, table, or count formats with field selection
|
||||
- **Flexible output** - Human-readable, JSON, or pretty JSON formats
|
||||
- **Filtering** - Powerful expression-based filtering (e.g., `size > 100`)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │
|
||||
│ ghidra ... │ │ Unix socket │ │ TCP to Ghidra │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ bridge.py │
|
||||
│ (Ghidra Script)│
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
All commands go through the daemon, which maintains a persistent connection to Ghidra via the bridge script. This provides:
|
||||
- **Consistent state** - Single Ghidra process for all operations
|
||||
- **Fast queries** - No JVM startup overhead per command
|
||||
- **Auto-start** - Daemon starts automatically when needed
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
@@ -43,14 +65,14 @@ ghidra config set ghidra_path /path/to/ghidra
|
||||
# Check installation
|
||||
ghidra doctor
|
||||
|
||||
# Import and analyze a binary
|
||||
# Import and analyze a binary (daemon auto-starts)
|
||||
ghidra quick ./binary
|
||||
|
||||
# Or step by step:
|
||||
ghidra import ./binary --project myproject --program mybinary
|
||||
ghidra analyze --project myproject --program mybinary
|
||||
|
||||
# Start the daemon for fast repeated queries
|
||||
ghidra daemon start --project myproject --program mybinary
|
||||
|
||||
# List functions
|
||||
# Query functions (uses running daemon)
|
||||
ghidra function list
|
||||
|
||||
# Decompile a function
|
||||
@@ -73,8 +95,9 @@ ghidra graph calls main --depth 3
|
||||
ghidra project create <name> # Create project
|
||||
ghidra project list # List projects
|
||||
ghidra project delete <name> # Delete project
|
||||
ghidra import <binary> --project <p> # Import binary
|
||||
ghidra import <binary> --project <p> # Import binary (auto-starts daemon)
|
||||
ghidra analyze --project <p> # Run analysis
|
||||
ghidra quick <binary> # Import + analyze in one step
|
||||
```
|
||||
|
||||
### Function Analysis
|
||||
@@ -149,36 +172,39 @@ ghidra stats # Program statistics
|
||||
ghidra summary # Program summary
|
||||
```
|
||||
|
||||
## Daemon Mode
|
||||
## Daemon Management
|
||||
|
||||
The daemon keeps Ghidra loaded in memory for fast queries:
|
||||
The daemon keeps Ghidra loaded in memory. It starts automatically when needed, but you can also control it manually:
|
||||
|
||||
```bash
|
||||
# Start daemon with a program loaded
|
||||
ghidra daemon start --project myproject --program mybinary
|
||||
|
||||
# All subsequent commands use the daemon automatically
|
||||
ghidra function list # Fast!
|
||||
ghidra decompile main # Fast!
|
||||
|
||||
# Check daemon status
|
||||
ghidra daemon status
|
||||
|
||||
# All commands use the daemon automatically
|
||||
ghidra function list # Fast!
|
||||
ghidra decompile main # Fast!
|
||||
|
||||
# Stop daemon
|
||||
ghidra daemon stop
|
||||
|
||||
# Restart with different program
|
||||
ghidra daemon restart --project myproject --program otherbinary
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
```bash
|
||||
# JSON output (default)
|
||||
ghidra function list --format json
|
||||
# Human-readable (default)
|
||||
ghidra function list
|
||||
|
||||
# Table format
|
||||
ghidra function list --format table
|
||||
# JSON output
|
||||
ghidra function list --json
|
||||
|
||||
# Count only
|
||||
ghidra function list --format count
|
||||
# Pretty JSON
|
||||
ghidra function list --pretty
|
||||
|
||||
# Select specific fields
|
||||
ghidra function list --fields "name,address,size"
|
||||
@@ -196,15 +222,15 @@ ghidra strings list --filter "length > 20"
|
||||
|
||||
## AI Agent Integration
|
||||
|
||||
Ghidra CLI is designed to work seamlessly with AI coding assistants like Claude Code. The structured JSON output and comprehensive command set make it ideal for automated reverse engineering workflows.
|
||||
Ghidra CLI is designed to work seamlessly with AI coding assistants like Claude Code. The structured output and comprehensive command set make it ideal for automated reverse engineering workflows.
|
||||
|
||||
Example workflow with an AI agent:
|
||||
1. `ghidra import suspicious.exe --project analysis --program suspicious`
|
||||
2. `ghidra analyze --project analysis --program suspicious`
|
||||
3. `ghidra daemon start --project analysis --program suspicious`
|
||||
4. `ghidra find interesting` - AI analyzes suspicious patterns
|
||||
5. `ghidra decompile <func>` - AI examines specific functions
|
||||
6. `ghidra x-ref to <addr>` - AI traces data flow
|
||||
1. `ghidra quick suspicious.exe` - Import, analyze, start daemon
|
||||
2. `ghidra find interesting` - AI analyzes suspicious patterns
|
||||
3. `ghidra decompile <func>` - AI examines specific functions
|
||||
4. `ghidra x-ref to <addr>` - AI traces data flow
|
||||
5. `ghidra patch nop <addr>` - AI patches anti-debug code
|
||||
6. `ghidra patch export` - Export patched binary
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Daemon Module
|
||||
|
||||
The daemon is the central execution authority for ghidra-cli. All commands route through the daemon, which maintains a persistent connection to Ghidra via the bridge.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ CLI Client │────▶│ IPC Server │────▶│ Handler │────▶│ GhidraBridge│
|
||||
│ (DaemonCli) │ │ (Unix sock) │ │ (Routing) │ │ (TCP→Ghidra)│
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ bridge.py │
|
||||
│ (In Ghidra) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `mod.rs` | Daemon main loop, startup, shutdown |
|
||||
| `ipc_server.rs` | Unix socket server, accepts client connections |
|
||||
| `handler.rs` | Routes IPC commands to bridge or specialized handlers |
|
||||
| `process.rs` | Daemon lifecycle, lock files, process management |
|
||||
| `queue.rs` | Command queue execution |
|
||||
| `cache.rs` | Result caching |
|
||||
| `state.rs` | Daemon state management |
|
||||
| `handlers/` | Specialized command handlers |
|
||||
|
||||
## Command Flow
|
||||
|
||||
1. **CLI sends command** via IPC (Unix socket)
|
||||
2. **IPC server** receives request, parses JSON
|
||||
3. **Handler** routes to appropriate processor:
|
||||
- Direct bridge commands (decompile, function list, etc.)
|
||||
- Import/Analyze commands (via bridge.py handlers)
|
||||
- ExecuteCli for generic CLI command forwarding
|
||||
4. **Bridge** sends to Ghidra via TCP, receives response
|
||||
5. **Response** flows back through IPC to CLI
|
||||
|
||||
## Auto-Start Behavior
|
||||
|
||||
Import, Analyze, and Quick commands auto-start the daemon:
|
||||
|
||||
1. CLI checks if daemon is running for project
|
||||
2. If not, starts daemon in background (`daemonize_unix` / `daemonize_windows`)
|
||||
3. Waits briefly for daemon to initialize
|
||||
4. Connects and sends command
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- **One daemon per project** - Lock file prevents duplicates
|
||||
- **One program per daemon** - Daemon loads a single program
|
||||
- **Graceful shutdown** - Handles SIGTERM, SIGINT, IPC shutdown command
|
||||
- **Lock files** - Located at `~/.local/share/ghidra-cli/daemon-{hash}.lock`
|
||||
- **Logs** - Located at `~/.local/share/ghidra-cli/daemon.log`
|
||||
|
||||
## Handlers
|
||||
|
||||
Specialized handlers in `handlers/` directory:
|
||||
|
||||
| Handler | Commands |
|
||||
|---------|----------|
|
||||
| `program.rs` | Program info, memory, imports, exports |
|
||||
| `symbols.rs` | Symbol operations |
|
||||
| `types.rs` | Data type operations |
|
||||
| `comments.rs` | Comment operations |
|
||||
| `graph.rs` | Call graph operations |
|
||||
| `find.rs` | Search operations |
|
||||
| `diff.rs` | Program diff operations |
|
||||
| `patch.rs` | Binary patching |
|
||||
| `script.rs` | Script execution |
|
||||
| `disasm.rs` | Disassembly |
|
||||
| `stats.rs` | Statistics |
|
||||
| `batch.rs` | Batch command execution |
|
||||
|
||||
## Bridge Commands
|
||||
|
||||
Commands sent to bridge.py in Ghidra:
|
||||
|
||||
- `import` - Import binary using AutoImporter
|
||||
- `analyze` - Trigger analysis using AutoAnalysisManager
|
||||
- `list_functions`, `decompile`, `list_strings`, etc.
|
||||
|
||||
See `src/ghidra/scripts/bridge.py` for the full command reference.
|
||||
@@ -97,6 +97,21 @@ async fn handle_command_inner(
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::Import { binary_path, project, program } => {
|
||||
execute_bridge_command(bridge, "import", Some(json!({
|
||||
"binary_path": binary_path,
|
||||
"project": project,
|
||||
"program": program,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::Analyze { project, program } => {
|
||||
execute_bridge_command(bridge, "analyze", Some(json!({
|
||||
"project": project,
|
||||
"program": program,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::ExecuteCli { command_json } => {
|
||||
// Deserialize and execute CLI command through the queue handlers
|
||||
let cli_command: crate::cli::Commands = serde_json::from_str(&command_json)
|
||||
|
||||
@@ -6,7 +6,6 @@ pub mod scripts;
|
||||
pub mod setup;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use crate::config::Config;
|
||||
use crate::error::{GhidraError, Result};
|
||||
|
||||
@@ -87,86 +86,6 @@ impl GhidraClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn import_binary(&self, project_name: &str, binary_path: &Path, program_name: Option<&str>) -> Result<String> {
|
||||
if !self.project_exists(project_name) {
|
||||
self.create_project(project_name)?;
|
||||
}
|
||||
|
||||
let program_name = program_name.unwrap_or_else(|| {
|
||||
binary_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("program")
|
||||
});
|
||||
|
||||
let project_path = self.get_project_path(project_name);
|
||||
let headless = self.get_headless_script();
|
||||
|
||||
let output = Command::new(&headless)
|
||||
.arg(project_path.to_str().unwrap())
|
||||
.arg(project_name)
|
||||
.arg("-import")
|
||||
.arg(binary_path.to_str().unwrap())
|
||||
.arg("-overwrite")
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GhidraError::ExecutionFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(program_name.to_string())
|
||||
}
|
||||
|
||||
pub fn analyze_program(&self, project_name: &str, program_name: &str) -> Result<()> {
|
||||
let project_path = self.get_project_path(project_name);
|
||||
let headless = self.get_headless_script();
|
||||
|
||||
let output = Command::new(&headless)
|
||||
.arg(project_path.to_str().unwrap())
|
||||
.arg(project_name)
|
||||
.arg("-process")
|
||||
.arg(program_name)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GhidraError::ExecutionFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_script(&self, project_name: &str, program_name: &str, script_path: &Path, args: &[String]) -> Result<String> {
|
||||
let project_path = self.get_project_path(project_name);
|
||||
let headless = self.get_headless_script();
|
||||
|
||||
let mut cmd = Command::new(&headless);
|
||||
cmd.arg(project_path.to_str().unwrap())
|
||||
.arg(project_name)
|
||||
.arg("-process")
|
||||
.arg(program_name)
|
||||
.arg("-scriptPath")
|
||||
.arg(script_path.parent().unwrap().to_str().unwrap())
|
||||
.arg("-postScript")
|
||||
.arg(script_path.file_name().unwrap().to_str().unwrap());
|
||||
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(GhidraError::ExecutionFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
fn get_scripts_dir(&self) -> Result<PathBuf> {
|
||||
let config_dir = dirs::config_dir()
|
||||
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
|
||||
|
||||
@@ -800,8 +800,86 @@ def handle_stats(args):
|
||||
except Exception as e:
|
||||
return {"error": "Failed to get stats: " + str(e)}
|
||||
|
||||
# --- Import/Analyze Handlers ---
|
||||
|
||||
def handle_import(args):
|
||||
"""Import a binary into the current project."""
|
||||
from ghidra.app.util.importer import AutoImporter
|
||||
from ghidra.util.task import ConsoleTaskMonitor
|
||||
from java.io import File
|
||||
|
||||
binary_path = args.get("binary_path")
|
||||
if not binary_path:
|
||||
return {"error": "No binary_path provided"}
|
||||
|
||||
program_name = args.get("program")
|
||||
if not program_name:
|
||||
binary_file = File(binary_path)
|
||||
program_name = binary_file.getName()
|
||||
|
||||
project = state.getProject()
|
||||
if project is None:
|
||||
return {"error": "No project open"}
|
||||
|
||||
try:
|
||||
binary_file = File(binary_path)
|
||||
if not binary_file.exists():
|
||||
return {"error": "Binary file not found: " + binary_path}
|
||||
|
||||
monitor = ConsoleTaskMonitor()
|
||||
project_data = project.getProjectData()
|
||||
|
||||
imported = AutoImporter.importByUsingBestGuess(
|
||||
binary_file,
|
||||
None,
|
||||
project_data.getRootFolder(),
|
||||
program_name,
|
||||
monitor
|
||||
)
|
||||
|
||||
if imported is None:
|
||||
return {"error": "Failed to import binary"}
|
||||
|
||||
return {"status": "success", "program": program_name}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": "Import failed: " + str(e)}
|
||||
|
||||
def handle_analyze(args):
|
||||
"""Trigger auto-analysis on the current program."""
|
||||
from ghidra.app.cmd.analysis import AutoAnalysisManager
|
||||
from ghidra.util.task import ConsoleTaskMonitor
|
||||
|
||||
program_name = args.get("program")
|
||||
if not program_name:
|
||||
return {"error": "No program name provided"}
|
||||
|
||||
if currentProgram is None:
|
||||
return {"error": "No program currently loaded"}
|
||||
|
||||
if currentProgram.getName() != program_name:
|
||||
return {"error": "Program mismatch: expected " + program_name + " but current is " + currentProgram.getName()}
|
||||
|
||||
try:
|
||||
monitor = ConsoleTaskMonitor()
|
||||
auto_mgr = AutoAnalysisManager.getAnalysisManager(currentProgram)
|
||||
|
||||
if auto_mgr is None:
|
||||
return {"error": "Could not get AutoAnalysisManager"}
|
||||
|
||||
auto_mgr.reAnalyzeAll(None)
|
||||
auto_mgr.startAnalysis(monitor)
|
||||
|
||||
return {"status": "success", "program": program_name}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": "Analysis failed: " + str(e)}
|
||||
|
||||
COMMANDS = {
|
||||
"ping": handle_ping,
|
||||
# Import/Analyze commands
|
||||
"import": handle_import,
|
||||
"analyze": handle_analyze,
|
||||
"program_info": handle_program_info,
|
||||
"program_close": handle_program_close,
|
||||
"program_delete": handle_program_delete,
|
||||
|
||||
@@ -152,6 +152,34 @@ impl DaemonClient {
|
||||
pub async fn execute_cli_json(&mut self, command_json: String) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ExecuteCli { command_json }).await
|
||||
}
|
||||
|
||||
/// Import a binary into a project.
|
||||
pub async fn import_binary(
|
||||
&mut self,
|
||||
binary_path: &str,
|
||||
project: &str,
|
||||
program: Option<&str>,
|
||||
) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::Import {
|
||||
binary_path: binary_path.to_string(),
|
||||
project: project.to_string(),
|
||||
program: program.map(|s| s.to_string()),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Analyze a program in a project.
|
||||
pub async fn analyze_program(
|
||||
&mut self,
|
||||
project: &str,
|
||||
program: &str,
|
||||
) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::Analyze {
|
||||
project: project.to_string(),
|
||||
program: program.to_string(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if daemon is running (without establishing a full connection).
|
||||
|
||||
@@ -112,6 +112,18 @@ pub enum Command {
|
||||
/// Get cross-references from an address
|
||||
XRefsFrom { address: String },
|
||||
|
||||
// === Project Management ===
|
||||
/// Import a binary into a project
|
||||
Import {
|
||||
binary_path: String,
|
||||
project: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
program: Option<String>,
|
||||
},
|
||||
|
||||
/// Analyze a program in a project
|
||||
Analyze { project: String, program: String },
|
||||
|
||||
// === Session Management ===
|
||||
/// Health check
|
||||
Ping,
|
||||
|
||||
+130
-77
@@ -51,14 +51,14 @@ fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
Commands::Init => handle_init(),
|
||||
Commands::Doctor => handle_doctor(),
|
||||
Commands::Version => handle_version(),
|
||||
Commands::Import(args) => handle_import(args),
|
||||
Commands::Analyze(args) => handle_analyze(args),
|
||||
Commands::Quick(args) => handle_quick(args),
|
||||
Commands::Config(cmd) => handle_config_command(cmd),
|
||||
Commands::SetDefault(args) => handle_set_default(args),
|
||||
Commands::Project(args) => handle_project_command(args.command),
|
||||
// Commands requiring daemon are handled by run_with_daemon_check
|
||||
Commands::Query(_)
|
||||
Commands::Import(_)
|
||||
| Commands::Analyze(_)
|
||||
| Commands::Quick(_)
|
||||
| Commands::Query(_)
|
||||
| Commands::Summary(_)
|
||||
| Commands::Function(_)
|
||||
| Commands::Strings(_)
|
||||
@@ -88,7 +88,10 @@ async fn run_async(cli: Cli) -> anyhow::Result<()> {
|
||||
fn requires_daemon(command: &Commands) -> bool {
|
||||
matches!(
|
||||
command,
|
||||
Commands::Query(_)
|
||||
Commands::Import(_)
|
||||
| Commands::Analyze(_)
|
||||
| Commands::Quick(_)
|
||||
| Commands::Query(_)
|
||||
| Commands::Decompile(_)
|
||||
| Commands::Function(_)
|
||||
| Commands::Strings(_)
|
||||
@@ -117,33 +120,147 @@ async fn run_with_daemon_check(cli: Cli) -> anyhow::Result<()> {
|
||||
return run(cli);
|
||||
}
|
||||
|
||||
// Query-type commands REQUIRE the daemon
|
||||
let config = Config::load()?;
|
||||
let project_path = match &cli.command {
|
||||
Commands::Import(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
Commands::Analyze(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
Commands::Quick(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
_ => {
|
||||
resolve_project_path(&None, &config)?
|
||||
}
|
||||
};
|
||||
|
||||
ensure_daemon_running(&project_path).await?;
|
||||
|
||||
match ipc::client::DaemonClient::connect().await {
|
||||
Ok(mut client) => {
|
||||
info!("Connected to daemon via IPC");
|
||||
let output = execute_via_daemon(&mut client, &cli.command).await?;
|
||||
if !output.is_empty() {
|
||||
println!("{}", output);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Error: This command requires the daemon to be running.");
|
||||
Err(e) => {
|
||||
eprintln!("Error: Failed to connect to daemon: {}", e);
|
||||
eprintln!();
|
||||
eprintln!("Start the daemon with:");
|
||||
eprintln!(" ghidra daemon start --project <project-name>");
|
||||
eprintln!();
|
||||
eprintln!("Or run a quick analysis first:");
|
||||
eprintln!(" ghidra quick <binary>");
|
||||
eprintln!("The daemon may still be starting. Try again in a moment.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure daemon is running for the given project path.
|
||||
async fn ensure_daemon_running(project_path: &PathBuf) -> anyhow::Result<()> {
|
||||
let data_dir = get_data_dir()?;
|
||||
|
||||
if get_running_daemon_info(&data_dir, project_path)?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = Config::load()?;
|
||||
let log_file = data_dir.join("daemon.log");
|
||||
|
||||
let daemon_config = DaemonConfig {
|
||||
project_path: project_path.clone(),
|
||||
ghidra_install_dir: config.ghidra_install_dir.map(PathBuf::from),
|
||||
log_file,
|
||||
program_name: config.default_program.clone(),
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
daemonize_unix(daemon_config, None)?;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
daemonize_windows(daemon_config, None)?;
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a command via the daemon IPC connection.
|
||||
async fn execute_via_daemon(
|
||||
client: &mut ipc::client::DaemonClient,
|
||||
command: &Commands,
|
||||
) -> anyhow::Result<String> {
|
||||
let result = match command {
|
||||
Commands::Import(args) => {
|
||||
let binary_path = PathBuf::from(&args.binary);
|
||||
if !binary_path.exists() {
|
||||
anyhow::bail!("Binary not found: {}", args.binary);
|
||||
}
|
||||
|
||||
let result = client.import_binary(
|
||||
&args.binary,
|
||||
&args.project.as_ref().unwrap_or(&"quick-analysis".to_string()),
|
||||
args.program.as_deref(),
|
||||
).await?;
|
||||
|
||||
if let Some(program_name) = result.as_str() {
|
||||
println!("Successfully imported as: {}", program_name);
|
||||
} else if let Some(program_name) = result.get("program").and_then(|p| p.as_str()) {
|
||||
println!("Successfully imported as: {}", program_name);
|
||||
}
|
||||
|
||||
return Ok(String::new());
|
||||
}
|
||||
Commands::Analyze(args) => {
|
||||
let config = Config::load()?;
|
||||
let program = resolve_program(&args.program, &config)?;
|
||||
let project = resolve_project(&args.project, &config, &program)?;
|
||||
|
||||
println!("Analyzing {}...", program);
|
||||
|
||||
client.analyze_program(&project, &program).await?;
|
||||
|
||||
println!("Analysis complete!");
|
||||
|
||||
return Ok(String::new());
|
||||
}
|
||||
Commands::Quick(args) => {
|
||||
let project = args.project.clone().unwrap_or_else(|| "quick-analysis".to_string());
|
||||
let binary_path = PathBuf::from(&args.binary);
|
||||
|
||||
println!("Quick analysis of {}...\n", args.binary);
|
||||
|
||||
println!("[1/3] Importing binary...");
|
||||
let result = client.import_binary(&args.binary, &project, None).await?;
|
||||
|
||||
let program_name = if let Some(name) = result.as_str() {
|
||||
name.to_string()
|
||||
} else if let Some(name) = result.get("program").and_then(|p| p.as_str()) {
|
||||
name.to_string()
|
||||
} else {
|
||||
binary_path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("program")
|
||||
.to_string()
|
||||
};
|
||||
|
||||
println!("[2/3] Running analysis...");
|
||||
client.analyze_program(&project, &program_name).await?;
|
||||
|
||||
println!("[3/3] Done!\n");
|
||||
println!("Analysis complete. To query the binary, start the daemon:");
|
||||
println!(" ghidra daemon start --project {} --program {}", project, program_name);
|
||||
println!("\nThen run queries like:");
|
||||
println!(" ghidra function list");
|
||||
println!(" ghidra decompile main");
|
||||
println!(" ghidra summary");
|
||||
|
||||
return Ok(String::new());
|
||||
}
|
||||
Commands::Query(args) => {
|
||||
match args.data_type.as_str() {
|
||||
"functions" => client.list_functions(args.limit, args.filter.clone()).await?,
|
||||
@@ -658,70 +775,6 @@ fn handle_version() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_import(args: cli::ImportArgs) -> anyhow::Result<()> {
|
||||
let config = Config::load()?;
|
||||
let client = GhidraClient::new(config.clone())?;
|
||||
|
||||
let project = resolve_project(&args.project, &config, &args.program.as_ref().unwrap_or(&"unknown".to_string()))?;
|
||||
|
||||
let binary_path = PathBuf::from(&args.binary);
|
||||
if !binary_path.exists() {
|
||||
anyhow::bail!(format!("Binary not found: {}", args.binary));
|
||||
}
|
||||
|
||||
println!("Importing {} into project {}...", args.binary, project);
|
||||
|
||||
let program_name = client.import_binary(&project, &binary_path, args.program.as_deref())?;
|
||||
|
||||
println!("Successfully imported as: {}", program_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_analyze(args: cli::AnalyzeArgs) -> anyhow::Result<()> {
|
||||
let config = Config::load()?;
|
||||
let client = GhidraClient::new(config.clone())?;
|
||||
|
||||
let program = resolve_program(&args.program, &config)?;
|
||||
let project = resolve_project(&args.project, &config, &program)?;
|
||||
|
||||
println!("Analyzing {}...", program);
|
||||
|
||||
client.analyze_program(&project, &program)?;
|
||||
|
||||
println!("Analysis complete!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_quick(args: cli::QuickArgs) -> anyhow::Result<()> {
|
||||
let config = Config::load()?;
|
||||
let client = GhidraClient::new(config.clone())?;
|
||||
|
||||
let project = args.project.unwrap_or_else(|| "quick-analysis".to_string());
|
||||
let binary_path = PathBuf::from(&args.binary);
|
||||
|
||||
println!("Quick analysis of {}...\n", args.binary);
|
||||
|
||||
// Import
|
||||
println!("[1/3] Importing binary...");
|
||||
let program_name = client.import_binary(&project, &binary_path, None)?;
|
||||
|
||||
// Analyze
|
||||
println!("[2/3] Running analysis...");
|
||||
client.analyze_program(&project, &program_name)?;
|
||||
|
||||
// Done
|
||||
println!("[3/3] Done!\n");
|
||||
println!("Analysis complete. To query the binary, start the daemon:");
|
||||
println!(" ghidra daemon start --project {} --program {}", project, program_name);
|
||||
println!("\nThen run queries like:");
|
||||
println!(" ghidra function list");
|
||||
println!(" ghidra decompile main");
|
||||
println!(" ghidra summary");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_config_command(cmd: cli::ConfigCommands) -> anyhow::Result<()> {
|
||||
use cli::ConfigCommands;
|
||||
|
||||
Reference in New Issue
Block a user