345 lines
10 KiB
C++
345 lines
10 KiB
C++
/**
|
|
* Integration test: Verify we can spawn and communicate with Ghidra plugin
|
|
*
|
|
* This test doesn't require Ghidra to be installed - it only tests the
|
|
* communication layer (process spawning, JSON-RPC over stdio).
|
|
*/
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <array>
|
|
#include <csignal>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
#include <yams/compat/unistd.h>
|
|
|
|
#ifndef _WIN32
|
|
#include <sys/wait.h>
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
struct PluginProcess {
|
|
pid_t pid = -1;
|
|
int stdinFd = -1;
|
|
int stdoutFd = -1;
|
|
int stderrFd = -1;
|
|
|
|
~PluginProcess() { cleanup(); }
|
|
|
|
void cleanup() {
|
|
if (stdinFd >= 0) {
|
|
close(stdinFd);
|
|
stdinFd = -1;
|
|
}
|
|
if (stdoutFd >= 0) {
|
|
close(stdoutFd);
|
|
stdoutFd = -1;
|
|
}
|
|
if (stderrFd >= 0) {
|
|
close(stderrFd);
|
|
stderrFd = -1;
|
|
}
|
|
|
|
if (pid > 0) {
|
|
#ifdef _WIN32
|
|
// No fork/kill/waitpid-based lifecycle on Windows.
|
|
pid = -1;
|
|
#else
|
|
if (kill(pid, SIGTERM) == 0) {
|
|
int status = 0;
|
|
(void)waitpid(pid, &status, 0);
|
|
}
|
|
pid = -1;
|
|
#endif
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Spawn plugin process with stdio pipes
|
|
*/
|
|
bool spawnPlugin(PluginProcess& proc, const fs::path& pluginPath, const fs::path& sdkPath) {
|
|
#ifdef _WIN32
|
|
(void)proc;
|
|
(void)pluginPath;
|
|
(void)sdkPath;
|
|
return false;
|
|
#else
|
|
int stdinPipe[2], stdoutPipe[2], stderrPipe[2];
|
|
|
|
if (pipe(stdinPipe) == -1 || pipe(stdoutPipe) == -1 || pipe(stderrPipe) == -1) {
|
|
return false;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
close(stdinPipe[0]);
|
|
close(stdinPipe[1]);
|
|
close(stdoutPipe[0]);
|
|
close(stdoutPipe[1]);
|
|
close(stderrPipe[0]);
|
|
close(stderrPipe[1]);
|
|
return false;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
// Child process
|
|
dup2(stdinPipe[0], STDIN_FILENO);
|
|
dup2(stdoutPipe[1], STDOUT_FILENO);
|
|
dup2(stderrPipe[1], STDERR_FILENO);
|
|
|
|
close(stdinPipe[0]);
|
|
close(stdinPipe[1]);
|
|
close(stdoutPipe[0]);
|
|
close(stdoutPipe[1]);
|
|
close(stderrPipe[0]);
|
|
close(stderrPipe[1]);
|
|
|
|
// Set PYTHONPATH environment variable
|
|
std::string pythonPath = sdkPath.string();
|
|
if (const char* existing = getenv("PYTHONPATH")) {
|
|
pythonPath += ":" + std::string(existing);
|
|
}
|
|
setenv("PYTHONPATH", pythonPath.c_str(), 1);
|
|
|
|
// Execute plugin
|
|
execlp("python3", "python3", pluginPath.c_str(), nullptr);
|
|
|
|
// If we get here, exec failed
|
|
std::cerr << "Failed to exec plugin: " << strerror(errno) << std::endl;
|
|
_exit(1);
|
|
}
|
|
|
|
// Parent process
|
|
close(stdinPipe[0]);
|
|
close(stdoutPipe[1]);
|
|
close(stderrPipe[1]);
|
|
|
|
proc.pid = pid;
|
|
proc.stdinFd = stdinPipe[1];
|
|
proc.stdoutFd = stdoutPipe[0];
|
|
proc.stderrFd = stderrPipe[0];
|
|
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Send JSON-RPC request and read response
|
|
*/
|
|
bool sendRequest(PluginProcess& proc, const nlohmann::json& request, nlohmann::json& response) {
|
|
// Write request
|
|
std::string requestStr = request.dump() + "\n";
|
|
ssize_t written = write(proc.stdinFd, requestStr.c_str(), requestStr.size());
|
|
if (written != static_cast<ssize_t>(requestStr.size())) {
|
|
return false;
|
|
}
|
|
|
|
// Read response line
|
|
std::string responseLine;
|
|
char buffer[4096];
|
|
while (true) {
|
|
ssize_t bytesRead = read(proc.stdoutFd, buffer, sizeof(buffer));
|
|
if (bytesRead <= 0) {
|
|
return false;
|
|
}
|
|
|
|
responseLine.append(buffer, bytesRead);
|
|
|
|
// Check for newline
|
|
auto newlinePos = responseLine.find('\n');
|
|
if (newlinePos != std::string::npos) {
|
|
responseLine = responseLine.substr(0, newlinePos);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parse response
|
|
try {
|
|
response = nlohmann::json::parse(responseLine);
|
|
return true;
|
|
} catch (const std::exception&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get the repository root directory - uses YAMS_SOURCE_DIR if available
|
|
fs::path getRepoRoot() {
|
|
// Try environment variable first (set by meson)
|
|
const char* source_dir = std::getenv("YAMS_SOURCE_DIR");
|
|
if (source_dir) {
|
|
return fs::path(source_dir);
|
|
}
|
|
|
|
// Fall back to absolute path from __FILE__
|
|
// __FILE__ = tests/integration/ghidra_plugin_communication_test.cpp
|
|
// We want the repo root (3 levels up)
|
|
fs::path file_path = fs::absolute(fs::path(__FILE__));
|
|
return file_path.parent_path().parent_path().parent_path();
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
TEST_CASE("Ghidra plugin - Spawn and handshake", "[integration][ghidra][plugin]") {
|
|
#ifdef _WIN32
|
|
SKIP("Ghidra plugin communication test uses POSIX process APIs");
|
|
#endif
|
|
// Find plugin and SDK paths
|
|
auto repoRoot = getRepoRoot();
|
|
auto pluginPath = repoRoot / "plugins" / "yams-ghidra-plugin" / "plugin.py";
|
|
auto sdkPath = repoRoot / "external" / "yams-sdk";
|
|
|
|
if (!fs::exists(pluginPath)) {
|
|
SUCCEED("Ghidra plugin not found at " + pluginPath.string());
|
|
return;
|
|
}
|
|
|
|
REQUIRE(fs::exists(pluginPath));
|
|
REQUIRE(fs::exists(sdkPath));
|
|
|
|
PluginProcess proc;
|
|
REQUIRE(spawnPlugin(proc, pluginPath, sdkPath));
|
|
|
|
SECTION("Handshake - manifest") {
|
|
nlohmann::json request = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "handshake.manifest"},
|
|
{"params", nlohmann::json::object()}};
|
|
|
|
nlohmann::json response;
|
|
REQUIRE(sendRequest(proc, request, response));
|
|
|
|
REQUIRE(response.contains("result"));
|
|
REQUIRE(response["result"]["name"] == "yams_ghidra");
|
|
REQUIRE(response["result"]["version"] == "0.1.0");
|
|
REQUIRE(response["result"]["interfaces"].is_array());
|
|
REQUIRE(response["result"]["interfaces"][0] == "content_extractor_v1");
|
|
}
|
|
|
|
SECTION("Health check before init") {
|
|
nlohmann::json request = {{"jsonrpc", "2.0"},
|
|
{"id", 2},
|
|
{"method", "plugin.health"},
|
|
{"params", nlohmann::json::object()}};
|
|
|
|
nlohmann::json response;
|
|
REQUIRE(sendRequest(proc, request, response));
|
|
|
|
REQUIRE(response.contains("result"));
|
|
// Status is "degraded" when pyghidra is not available, "ok" when it is
|
|
auto status = response["result"]["status"].get<std::string>();
|
|
REQUIRE((status == "ok" || status == "degraded"));
|
|
REQUIRE(response["result"]["started"] == false);
|
|
}
|
|
|
|
SECTION("Plugin initialization") {
|
|
nlohmann::json request = {{"jsonrpc", "2.0"},
|
|
{"id", 3},
|
|
{"method", "plugin.init"},
|
|
{"params", {{"config", nlohmann::json::object()}}}};
|
|
|
|
nlohmann::json response;
|
|
REQUIRE(sendRequest(proc, request, response));
|
|
|
|
// Plugin should initialize (may or may not require pyghidra installed)
|
|
REQUIRE(response.contains("result"));
|
|
REQUIRE(response["result"]["status"] == "initialized");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Ghidra plugin - Process lifecycle", "[integration][ghidra][plugin]") {
|
|
#ifdef _WIN32
|
|
SKIP("Ghidra plugin communication test uses POSIX process APIs");
|
|
#endif
|
|
auto repoRoot = getRepoRoot();
|
|
auto pluginPath = repoRoot / "plugins" / "yams-ghidra-plugin" / "plugin.py";
|
|
auto sdkPath = repoRoot / "external" / "yams-sdk";
|
|
|
|
if (!fs::exists(pluginPath)) {
|
|
SUCCEED("Ghidra plugin not found at " + pluginPath.string());
|
|
return;
|
|
}
|
|
|
|
SECTION("Multiple requests on same process") {
|
|
PluginProcess proc;
|
|
REQUIRE(spawnPlugin(proc, pluginPath, sdkPath));
|
|
|
|
// Send multiple requests
|
|
for (int i = 1; i <= 3; ++i) {
|
|
nlohmann::json request = {{"jsonrpc", "2.0"},
|
|
{"id", i},
|
|
{"method", "plugin.health"},
|
|
{"params", nlohmann::json::object()}};
|
|
|
|
nlohmann::json response;
|
|
REQUIRE(sendRequest(proc, request, response));
|
|
REQUIRE(response["id"] == i);
|
|
REQUIRE(response.contains("result"));
|
|
}
|
|
}
|
|
|
|
SECTION("Process cleans up on destruction") {
|
|
{
|
|
PluginProcess proc;
|
|
REQUIRE(spawnPlugin(proc, pluginPath, sdkPath));
|
|
REQUIRE(proc.pid > 0);
|
|
// proc goes out of scope, should cleanup
|
|
}
|
|
// If we get here without hanging, cleanup worked
|
|
REQUIRE(true);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Ghidra plugin - Error handling", "[integration][ghidra][plugin]") {
|
|
#ifdef _WIN32
|
|
SKIP("Ghidra plugin communication test uses POSIX process APIs");
|
|
#endif
|
|
auto repoRoot = getRepoRoot();
|
|
auto pluginPath = repoRoot / "plugins" / "yams-ghidra-plugin" / "plugin.py";
|
|
auto sdkPath = repoRoot / "external" / "yams-sdk";
|
|
|
|
if (!fs::exists(pluginPath)) {
|
|
SUCCEED("Ghidra plugin not found at " + pluginPath.string());
|
|
return;
|
|
}
|
|
|
|
PluginProcess proc;
|
|
REQUIRE(spawnPlugin(proc, pluginPath, sdkPath));
|
|
|
|
SECTION("Invalid JSON-RPC request") {
|
|
nlohmann::json request = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "nonexistent.method"},
|
|
{"params", nlohmann::json::object()}};
|
|
|
|
nlohmann::json response;
|
|
REQUIRE(sendRequest(proc, request, response));
|
|
|
|
// Should return method not found error
|
|
REQUIRE(response.contains("error"));
|
|
REQUIRE(response["error"]["code"] == -32601); // Method not found
|
|
}
|
|
|
|
SECTION("Missing required params") {
|
|
nlohmann::json request = {
|
|
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "ghidra.analyze"} // Missing params
|
|
};
|
|
|
|
nlohmann::json response;
|
|
// Plugin might handle this differently, just verify it doesn't crash
|
|
bool sent = sendRequest(proc, request, response);
|
|
if (sent) {
|
|
// Should have error or result
|
|
REQUIRE((response.contains("error") || response.contains("result")));
|
|
}
|
|
}
|
|
}
|