├── Module.manifest
├── lib
├── .gitignore
└── README.txt
├── src
├── reva_cli
│ ├── .gitignore
│ ├── __init__.py
│ ├── launcher.py
│ └── stdio_bridge.py
├── main
│ ├── resources
│ │ └── images
│ │ │ └── README.txt
│ ├── help
│ │ └── help
│ │ │ ├── topics
│ │ │ └── skeleton
│ │ │ │ └── help.html
│ │ │ └── TOC_Source.xml
│ └── java
│ │ └── reva
│ │ ├── tools
│ │ ├── ProgramValidationException.java
│ │ └── ToolProvider.java
│ │ ├── plugin
│ │ ├── config
│ │ │ ├── ConfigurationBackendListener.java
│ │ │ ├── ConfigurationBackend.java
│ │ │ ├── InMemoryBackend.java
│ │ │ └── ToolOptionsBackend.java
│ │ ├── ConfigChangeListener.java
│ │ ├── RevaPlugin.java
│ │ └── RevaApplicationPlugin.java
│ │ ├── util
│ │ ├── SymbolUtil.java
│ │ ├── RevaInternalServiceRegistry.java
│ │ ├── SimilarityComparator.java
│ │ ├── DebugLogger.java
│ │ └── MemoryUtil.java
│ │ ├── resources
│ │ ├── ResourceProvider.java
│ │ ├── AbstractResourceProvider.java
│ │ └── impl
│ │ │ └── ProgramListResource.java
│ │ ├── revaAnalyzer.java
│ │ ├── revaExporter.java
│ │ ├── services
│ │ └── RevaMcpService.java
│ │ ├── ui
│ │ └── RevaProvider.java
│ │ ├── revaFileSystem.java
│ │ └── server
│ │ └── ApiKeyAuthFilter.java
├── test.slow
│ ├── java
│ │ └── reva
│ │ │ ├── CLAUDE.md
│ │ │ ├── plugin
│ │ │ ├── RevaPluginIntegrationTest.java
│ │ │ └── ConfigManagerSecurityTest.java
│ │ │ └── tools
│ │ │ ├── memory
│ │ │ └── MemoryToolProviderIntegrationTest.java
│ │ │ ├── comments
│ │ │ └── CommentToolProviderIntegrationTest.java
│ │ │ ├── bookmarks
│ │ │ └── BookmarkToolProviderIntegrationTest.java
│ │ │ └── data
│ │ │ └── DataToolProviderIntegrationTest.java
│ ├── resources
│ │ └── logback.xml
│ └── CLAUDE.md
├── test
│ └── java
│ │ └── reva
│ │ ├── util
│ │ └── RevaPluginTest.java
│ │ ├── RevaHeadlessIntegrationTestBase.java
│ │ ├── plugin
│ │ └── RevaPluginUnitTest.java
│ │ └── tools
│ │ └── strings
│ │ └── StringToolProviderTest.java
└── CLAUDE.md
├── ghidra_scripts
├── README.txt
├── sample_script.py
└── SampleScript.java
├── extension.properties
├── .continue
└── docs
│ ├── new-doc-1.yaml
│ └── new-doc.yaml
├── ReVa
├── .mcp.json
└── .claude-plugin
│ └── plugin.json
├── .claude
├── hooks
│ ├── setup-gradle.sh
│ ├── clone-ghidra-source.sh
│ └── install-ghidra-binary.sh
├── settings.json
├── README.md
└── agents
│ └── reva-setup-installer.md
├── .gitignore
├── data
├── sleighArgs.txt
├── languages
│ ├── skel.opinion
│ ├── skel.ldefs
│ ├── skel.pspec
│ ├── skel.slaspec
│ ├── skel.cspec
│ └── skel.sinc
├── README.txt
└── buildLanguage.xml
├── .vscode
├── extensions.json
└── launch.json
├── os
├── linux_x86_64
│ └── README.txt
├── mac_x86_64
│ └── README.txt
└── win_x86_64
│ └── README.txt
├── tests
├── requirements.txt
├── __init__.py
├── test_pyghidra.py
├── test_config.py
├── test_launcher.py
└── test_mcp_tools.py
├── .claude-plugin
└── marketplace.json
├── .github
├── dependabot.yml
├── workflows
│ ├── claude.yml
│ ├── test-headless.yml
│ ├── publish-ghidra.yml
│ ├── publish-pypi.yml
│ └── test-ghidra.yml
└── copilot-instructions.md
├── config
└── reva-headless-example.properties
└── pyproject.toml
/Module.manifest:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | *.jar
--------------------------------------------------------------------------------
/src/reva_cli/.gitignore:
--------------------------------------------------------------------------------
1 | _version.py
2 |
--------------------------------------------------------------------------------
/ghidra_scripts/README.txt:
--------------------------------------------------------------------------------
1 | Java source directory to hold module-specific Ghidra scripts.
2 |
--------------------------------------------------------------------------------
/extension.properties:
--------------------------------------------------------------------------------
1 | name=ReVa
2 | description=Reverse Engineering Assistant
3 | author=CyberKaida
4 | createdOn=
5 | version=@extversion@
6 |
--------------------------------------------------------------------------------
/src/main/resources/images/README.txt:
--------------------------------------------------------------------------------
1 | The "src/resources/images" directory is intended to hold all image/icon files used by
2 | this module.
3 |
--------------------------------------------------------------------------------
/.continue/docs/new-doc-1.yaml:
--------------------------------------------------------------------------------
1 | name: New doc
2 | version: 0.0.1
3 | schema: v1
4 | docs:
5 | - name: New docs
6 | startUrl: https://docs.continue.dev
7 |
--------------------------------------------------------------------------------
/.continue/docs/new-doc.yaml:
--------------------------------------------------------------------------------
1 | name: Model Context Protocol Java
2 | version: 0.0.1
3 | schema: v1
4 | docs:
5 | - name: MCP
6 | startUrl: https://modelcontextprotocol.io/sdk/java/mcp-overview
7 |
--------------------------------------------------------------------------------
/ghidra_scripts/sample_script.py:
--------------------------------------------------------------------------------
1 | # Sample PyGhidra GhidraScript
2 | # @category Examples
3 | # @runtime PyGhidra
4 |
5 | from java.util import LinkedList
6 | java_list = LinkedList([1,2,3])
7 |
8 | block = currentProgram.memory.getBlock('.text')
9 |
--------------------------------------------------------------------------------
/lib/README.txt:
--------------------------------------------------------------------------------
1 | The "lib" directory is intended to hold Jar files which this module is dependent upon. Jar files
2 | may be placed in this directory manually, or automatically by maven via the dependencies block
3 | of this module's build.gradle file.
--------------------------------------------------------------------------------
/ReVa/.mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "ReVa": {
4 | "command": "uv",
5 | "args": ["run", "mcp-reva"],
6 | "env": {
7 | "UV_WORKING_DIRECTORY": "${CLAUDE_PLUGIN_ROOT}/../"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.claude/hooks/setup-gradle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -ex
3 |
4 | # Log to both stderr and log file
5 | LOG_FILE="/tmp/reva-setup-gradle.log"
6 | exec > >(tee -a "${LOG_FILE}") 2>&1
7 |
8 | # Verify Java and Gradle
9 | java -version
10 | gradle --version
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | dist/
4 | bin/
5 | *.egg-info
6 | *.pyc
7 | *.class
8 | .venv
9 | .vim/
10 | .DS_Store
11 | .mypy_cache/
12 | __pycache__
13 |
14 | # Java heap dump files
15 | *.hprof
16 |
17 | # ReVa local project directories
18 | .reva/
--------------------------------------------------------------------------------
/data/sleighArgs.txt:
--------------------------------------------------------------------------------
1 | # Add sleigh compiler options to this file (one per line) which will
2 | # be used when compiling each language within this module.
3 | # All options should start with a '-' character.
4 | #
5 | # IMPORTANT: The -a option should NOT be specified
6 | #
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-python.mypy-type-checker",
4 | "vscjava.vscode-gradle",
5 | "ms-python.vscode-pylance",
6 | "ms-python.python",
7 | "vscjava.vscode-java-pack"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/os/linux_x86_64/README.txt:
--------------------------------------------------------------------------------
1 | The "os/linux_x86_64" directory is intended to hold Linux native binaries
2 | which this module is dependent upon. This directory may be eliminated for a specific
3 | module if native binaries are not provided for the corresponding platform.
4 |
--------------------------------------------------------------------------------
/os/mac_x86_64/README.txt:
--------------------------------------------------------------------------------
1 | The "os/mac_x86_64" directory is intended to hold macOS (OS X) native binaries
2 | which this module is dependent upon. This directory may be eliminated for a specific
3 | module if native binaries are not provided for the corresponding platform.
4 |
--------------------------------------------------------------------------------
/os/win_x86_64/README.txt:
--------------------------------------------------------------------------------
1 | The "os/win_x86_64" directory is intended to hold MS Windows native binaries (.exe)
2 | which this module is dependent upon. This directory may be eliminated for a specific
3 | module if native binaries are not provided for the corresponding platform.
4 |
--------------------------------------------------------------------------------
/ReVa/.claude-plugin/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ReVa",
3 | "description": "ReVa (Reverse Engineering Assistant) - AI-assisted binary analysis and reverse engineering with Ghidra integration",
4 | "version": "0.1.0",
5 | "author": {
6 | "name": "CyberKaida"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ghidra_scripts/SampleScript.java:
--------------------------------------------------------------------------------
1 | // Sample Java GhidraScript
2 | // @category Examples
3 | import ghidra.app.script.GhidraScript;
4 |
5 | public class SampleScript extends GhidraScript {
6 |
7 | @Override
8 | protected void run() throws Exception {
9 | println("Sample script!");
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/CLAUDE.md:
--------------------------------------------------------------------------------
1 | - When writing integration tests, make sure to set up the program, call the tool and then validate the output. If the tool modifies the program, validate the modification.
2 | - Don't write useless tests, make sure they have a purpose.
3 | - **CRITICAL**: Integration tests should validate actual Ghidra program state changes, not just MCP tool responses
4 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | # ReVa Headless Integration Test Dependencies
2 |
3 | # PyGhidra for headless Ghidra integration
4 | pyghidra>=1.0.0
5 |
6 | # MCP SDK for proper client communication
7 | mcp>=1.0.0
8 | httpx>=0.27.0
9 | httpx-sse>=0.4.0
10 |
11 | # Pytest framework
12 | pytest>=7.0.0
13 | pytest-timeout>=2.0.0
14 |
15 | # Optional: Better test output
16 | pytest-sugar>=0.9.0
17 |
--------------------------------------------------------------------------------
/.claude-plugin/marketplace.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reva",
3 | "description": "ReVa - AI-assisted binary analysis and reverse engineering with Ghidra integration",
4 | "owner": {
5 | "name": "CyberKaida"
6 | },
7 | "plugins": [
8 | {
9 | "name": "ReVa",
10 | "source": "./ReVa",
11 | "description": "AI-assisted binary analysis and reverse engineering with Ghidra integration"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/reva_cli/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | ReVa CLI - stdio MCP bridge for ReVa Ghidra extension.
3 |
4 | This package provides a command-line interface that bridges stdio MCP transport
5 | to ReVa's StreamableHTTP server, enabling seamless integration with Claude CLI.
6 | """
7 |
8 | try:
9 | from ._version import version as __version__
10 | except ImportError:
11 | # Fallback version if not installed or in development without git tags
12 | __version__ = "0.0.0.dev0"
13 |
--------------------------------------------------------------------------------
/.claude/hooks/clone-ghidra-source.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -ex
3 |
4 | # Log to both stderr and log file
5 | LOG_FILE="/tmp/reva-clone-ghidra.log"
6 | exec > >(tee -a "${LOG_FILE}") 2>&1
7 |
8 | GHIDRA_GIT=$(readlink -f "${CLAUDE_PROJECT_DIR}/../ghidra")
9 |
10 | if [ ! -d "${GHIDRA_GIT}" ]; then
11 | echo "The ghidra code is not at ${GHIDRA_GIT} use the @reva-setup-installer to fix this"
12 | else
13 | echo "Ghidra source already exists at ${GHIDRA_GIT}"
14 | fi
15 |
--------------------------------------------------------------------------------
/.claude/hooks/install-ghidra-binary.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -ex
3 |
4 | # Log to both stderr and log file
5 | LOG_FILE="/tmp/reva-install-ghidra.log"
6 | exec > >(tee -a "${LOG_FILE}") 2>&1
7 |
8 | # Only run in remote (web) environments
9 | if [ -n "${GHIDRA_INSTALL_DIR}" ]; then
10 | echo "GHIDRA_INSTALL_DIR is not set, use @reva-setup-installer to install the Ghidra binary distribution"
11 | fi
12 |
13 | # Persist environment variables for all subsequent bash commands
14 | if [ -n "$CLAUDE_ENV_FILE" ]; then
15 | echo "Make sure to add an export command for GHIDRA_INSTALL_DIR to ${CLAUDE_ENV_FILE}"
16 | fi
17 |
--------------------------------------------------------------------------------
/data/languages/skel.opinion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
--------------------------------------------------------------------------------
/data/languages/skel.ldefs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
--------------------------------------------------------------------------------
/data/README.txt:
--------------------------------------------------------------------------------
1 | The "data" directory is intended to hold data files that will be used by this module and will
2 | not end up in the .jar file, but will be present in the zip or tar file. Typically, data
3 | files are placed here rather than in the resources directory if the user may need to edit them.
4 |
5 | An optional data/languages directory can exist for the purpose of containing various Sleigh language
6 | specification files and importer opinion files.
7 |
8 | The data/buildLanguage.xml is used for building the contents of the data/languages directory.
9 |
10 | The skel language definition has been commented-out within the skel.ldefs file so that the
11 | skeleton language does not show-up within Ghidra.
12 |
13 | See the Sleigh language documentation (docs/languages/index.html) for details Sleigh language
14 | specification syntax.
15 |
--------------------------------------------------------------------------------
/src/test/java/reva/util/RevaPluginTest.java:
--------------------------------------------------------------------------------
1 | package reva.util;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import org.junit.Test;
6 |
7 | import reva.plugin.RevaPlugin;
8 |
9 | public class RevaPluginTest {
10 |
11 | @Test
12 | public void testPluginClassExists() {
13 | // Basic test to ensure the RevaPlugin class exists and can be instantiated
14 | assertNotNull("RevaPlugin class should exist", RevaPlugin.class);
15 | assertEquals("Package should be correct", "reva.plugin", RevaPlugin.class.getPackage().getName());
16 | }
17 |
18 | @Test
19 | public void testPluginConstructorSignature() throws NoSuchMethodException {
20 | // Verify the plugin has the correct constructor signature for Ghidra plugins
21 | assertNotNull("RevaPlugin should have a constructor that takes PluginTool",
22 | RevaPlugin.class.getConstructor(ghidra.framework.plugintool.PluginTool.class));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data/languages/skel.pspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/main/help/help/topics/skeleton/help.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 | Skeleton Help File for a Module
13 |
14 |
15 |
16 |
17 | Skeleton Help File for a Module
18 |
19 | This is a simple skeleton help topic. For a better description of what should and should not
20 | go in here, see the "sample" Ghidra extension in the Extensions/Ghidra directory, or see your
21 | favorite help topic. In general, language modules do not have their own help topics.
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.claude/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": {
3 | "additionalDirectories": ["../ghidra"],
4 | "allow": [
5 | "Search(path:../ghidra)",
6 | "Read(../ghidra/**)"
7 | ],
8 | "deny": []
9 | },
10 | "hooks": {
11 | "SessionStart": [
12 | {
13 | "matcher": "startup",
14 | "hooks": [
15 | {
16 | "type": "command",
17 | "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/clone-ghidra-source.sh"
18 | },
19 | {
20 | "type": "command",
21 | "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/install-ghidra-binary.sh"
22 | },
23 | {
24 | "type": "command",
25 | "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/setup-gradle.sh"
26 | }
27 | ]
28 | }
29 | ],
30 | "PostToolUse": [
31 | {
32 | "matcher": "Edit|MultiEdit|Write",
33 | "hooks": [
34 | {
35 | "type": "command",
36 | "command": "gradle compileJava"
37 | }
38 | ]
39 | }
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Dependabot configuration for reverse-engineering-assistant
2 | # For more information see: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | # Enable version updates for Gradle dependencies
7 | - package-ecosystem: "gradle"
8 | directory: "/" # Location of build.gradle
9 | schedule:
10 | interval: "weekly"
11 | day: "monday"
12 | time: "09:00"
13 | open-pull-requests-limit: 10
14 | labels:
15 | - "dependencies"
16 | - "gradle"
17 | commit-message:
18 | prefix: "chore"
19 | include: "scope"
20 |
21 | # Enable version updates for GitHub Actions
22 | - package-ecosystem: "github-actions"
23 | directory: "/" # GitHub Actions workflows are in .github/workflows/
24 | schedule:
25 | interval: "weekly"
26 | day: "monday"
27 | time: "09:00"
28 | open-pull-requests-limit: 5
29 | labels:
30 | - "dependencies"
31 | - "github-actions"
32 | commit-message:
33 | prefix: "chore"
34 | include: "scope"
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | ReVa Headless Integration Tests
3 |
4 | This package contains integration tests for ReVa's headless mode using PyGhidra.
5 |
6 | These tests verify that ReVa components work together in headless mode:
7 | - PyGhidra can initialize Ghidra
8 | - RevaHeadlessLauncher can start/stop servers
9 | - MCP tools are accessible and functional
10 | - Configuration files are loaded correctly
11 |
12 | Test Structure:
13 | - test_pyghidra.py - PyGhidra integration verification
14 | - test_launcher.py - RevaHeadlessLauncher lifecycle tests
15 | - test_mcp_tools.py - MCP tool connectivity and functionality
16 | - test_config.py - Configuration file loading tests
17 |
18 | Fixtures (conftest.py):
19 | - ghidra_initialized - One-time PyGhidra initialization (session scope)
20 | - test_program - Shared test program with memory and strings (session scope)
21 | - server - Start/stop server for each test (function scope)
22 | - mcp_client - MCP request helper (function scope)
23 |
24 | Usage:
25 | pytest tests/ -v
26 | pytest tests/test_launcher.py -v
27 | pytest tests/ -k "config" -v
28 | pytest tests/ --timeout=60
29 | """
30 |
--------------------------------------------------------------------------------
/src/test.slow/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/claude.yml:
--------------------------------------------------------------------------------
1 | name: Claude Code
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 | pull_request_review_comment:
7 | types: [created]
8 | issues:
9 | types: [opened, assigned]
10 | pull_request_review:
11 | types: [submitted]
12 |
13 | jobs:
14 | claude:
15 | if: |
16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20 | runs-on: ubuntu-latest
21 | permissions:
22 | contents: write
23 | pull-requests: write
24 | issues: write
25 | id-token: write
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v5
29 | with:
30 | fetch-depth: 1
31 |
32 | - name: Run Claude Code
33 | id: claude
34 | uses: anthropics/claude-code-action@beta
35 | with:
36 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/reva/tools/ProgramValidationException.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools;
17 |
18 | /**
19 | * Exception thrown when program validation fails.
20 | * This exception is used to indicate various program-related errors such as:
21 | * - Program not found
22 | * - Program is in an invalid state (e.g., closed)
23 | * - Invalid program path provided
24 | */
25 | public class ProgramValidationException extends RuntimeException {
26 |
27 | public ProgramValidationException(String message) {
28 | super(message);
29 | }
30 |
31 | public ProgramValidationException(String message, Throwable cause) {
32 | super(message, cause);
33 | }
34 | }
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | - This is a Ghidra extensions for Ghidra 12.0 and later called "ReVa", the Reverse Engineering Assistant.
2 | - The extension provides a Model Context Protocol (MCP) server for Ghidra. The server is implemented in Java and uses the MCP Java SDK.
3 | - Remember that MCP is in development, so make sure to check the MCP documentation for the latest information.
4 |
5 | Some good resources include:
6 | - [The MCP Java SDK](https://modelcontextprotocol.io/sdk/java/mcp-server)
7 | - [Ghidra on GitHub](https://github.com/NationalSecurityAgency/ghidra)
8 |
9 | - If you want to find a Ghidra API, start from the FlatProgramAPI or the ProgramPlugin API in the Ghidra repo. Use the GitHub tools to search for Ghidra documentation.
10 | - We don't use a gradle wrapper, so just run `gradle` in the root directory to build the project. We do have the Java tools installed in VSCode so you can just check for errors with VSCode instead.
11 | - When writing tests, use the Ghidra test framework. The tests are in the `src/test` directory. It is easy to run them with `gradle test --info` and the integration tests with `gradle integrationTest --info`.
12 | - You can use standard gradle test filtering to run specific tests with both of the test targets.
13 | - We target Ghidra 12.0 and later. Note we should use Java 21.
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/config/ConfigurationBackendListener.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin.config;
17 |
18 | /**
19 | * Listener interface for configuration backend changes.
20 | * Backends that support change notifications will call this when values change.
21 | */
22 | public interface ConfigurationBackendListener {
23 |
24 | /**
25 | * Called when a configuration value changes
26 | * @param category The configuration category
27 | * @param name The configuration name
28 | * @param oldValue The previous value
29 | * @param newValue The new value
30 | */
31 | void onConfigurationChanged(String category, String name, Object oldValue, Object newValue);
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/ConfigChangeListener.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | /**
19 | * Interface for listening to configuration changes in the ReVa plugin.
20 | * Implementations can register with ConfigManager to receive notifications
21 | * when configuration values change.
22 | */
23 | public interface ConfigChangeListener {
24 |
25 | /**
26 | * Called when a configuration option has changed.
27 | *
28 | * @param category The category of the option that changed
29 | * @param name The name of the option that changed
30 | * @param oldValue The previous value of the option
31 | * @param newValue The new value of the option
32 | */
33 | void onConfigChanged(String category, String name, Object oldValue, Object newValue);
34 | }
--------------------------------------------------------------------------------
/src/main/java/reva/util/SymbolUtil.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.util;
17 |
18 | import java.util.regex.Pattern;
19 |
20 | /**
21 | * Utility methods for working with Ghidra symbols.
22 | */
23 | public class SymbolUtil {
24 | // Regular expressions for Ghidra's default naming patterns
25 | private static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile(
26 | "^(FUN|LAB|SUB|DAT|EXT|PTR|ARRAY)_[0-9a-fA-F]+$"
27 | );
28 |
29 | /**
30 | * Check if a symbol name appears to be a default Ghidra-generated name
31 | * @param name The symbol name to check
32 | * @return True if the name follows Ghidra's default naming patterns
33 | */
34 | public static boolean isDefaultSymbolName(String name) {
35 | if (name == null) {
36 | return false;
37 | }
38 |
39 | return DEFAULT_NAME_PATTERN.matcher(name).matches();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/data/languages/skel.slaspec:
--------------------------------------------------------------------------------
1 | # sleigh specification file for Skeleton Processor
2 | # >> see docs/languages/sleigh.htm or sleigh.pdf for Sleigh syntax
3 | # Other language modules (see Ghidra/Processors) may provide better examples
4 | # when creating a new language module.
5 |
6 | define endian=little;
7 | define alignment=1;
8 |
9 | define space ram type=ram_space size=2 default;
10 |
11 | define space io type=ram_space size=2;
12 | define space register type=register_space size=1;
13 |
14 | define register offset=0x00 size=1 [ F A C B E D L H I R ];
15 | define register offset=0x00 size=2 [ AF BC DE HL ];
16 | define register offset=0x20 size=1 [ A_ F_ B_ C_ D_ E_ H_ L_ ]; # Alternate registers
17 | define register offset=0x20 size=2 [ AF_ BC_ DE_ HL_ ]; # Alternate registers
18 |
19 | define register offset=0x40 size=2 [ _ PC SP IX IY ];
20 |
21 | define register offset=0x50 size=1 [ rCBAR rCBR rBBR ];
22 |
23 | # Define context bits (if defined, size must be multiple of 4-bytes)
24 | define register offset=0xf0 size=4 contextreg;
25 |
26 | define context contextreg
27 | assume8bitIOSpace = (0,0)
28 | ;
29 |
30 | # Flag bits (?? manual is very confusing - could be typos!)
31 | @define C_flag "F[0,1]" # C: Carry
32 | @define N_flag "F[1,1]" # N: Add/Subtract
33 | @define PV_flag "F[2,1]" # PV: Parity/Overflow
34 | @define H_flag "F[4,1]" # H: Half Carry
35 | @define Z_flag "F[6,1]" # Z: Zero
36 | @define S_flag "F[7,1]" # S: Sign
37 |
38 | # Include contents of skel.sinc file
39 | @include "skel.sinc"
40 |
--------------------------------------------------------------------------------
/src/main/java/reva/tools/ToolProvider.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools;
17 |
18 | import ghidra.program.model.listing.Program;
19 |
20 | /**
21 | * Interface for MCP tool providers.
22 | * Tool providers are responsible for registering and managing
23 | * MCP tools that allow interactive operations with Ghidra data.
24 | */
25 | public interface ToolProvider {
26 | /**
27 | * Register all tools with the MCP server
28 | */
29 | void registerTools();
30 |
31 | /**
32 | * Notify the provider that a program has been opened
33 | * @param program The program that was opened
34 | */
35 | void programOpened(Program program);
36 |
37 | /**
38 | * Notify the provider that a program has been closed
39 | * @param program The program that was closed
40 | */
41 | void programClosed(Program program);
42 |
43 | /**
44 | * Clean up any resources or state
45 | */
46 | void cleanup();
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/reva/resources/ResourceProvider.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.resources;
17 |
18 | import ghidra.program.model.listing.Program;
19 |
20 | /**
21 | * Interface for MCP resource providers.
22 | * Resource providers are responsible for registering and managing
23 | * MCP resources that provide read-only access to Ghidra data.
24 | */
25 | public interface ResourceProvider {
26 | /**
27 | * Register all resources with the MCP server
28 | */
29 | void register();
30 |
31 | /**
32 | * Notify the provider that a program has been opened
33 | * @param program The program that was opened
34 | */
35 | void programOpened(Program program);
36 |
37 | /**
38 | * Notify the provider that a program has been closed
39 | * @param program The program that was closed
40 | */
41 | void programClosed(Program program);
42 |
43 | /**
44 | * Clean up any resources or state
45 | */
46 | void cleanup();
47 | }
48 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "java",
6 | "name": "Ghidra",
7 | "request": "launch",
8 | "mainClass": "ghidra.Ghidra",
9 | "args": "ghidra.GhidraRun",
10 | "classPaths": [
11 | "${env:GHIDRA_INSTALL_DIR}/Ghidra/Framework/Utility/lib/Utility.jar"
12 | ],
13 | "vmArgs": [
14 | "-Dghidra.external.modules=${workspaceFolder}",
15 | "-Djava.system.class.loader=ghidra.GhidraClassLoader",
16 | "-Dfile.encoding=UTF8",
17 | "-Duser.country=US",
18 | "-Duser.language=en",
19 | "-Duser.variant=",
20 | "-Dsun.java2d.opengl=false",
21 | "-Djdk.tls.client.protocols=TLSv1.2,TLSv1.3",
22 | "-Dcpu.core.limit=",
23 | "-Dcpu.core.override=",
24 | "-Dfont.size.override=",
25 | "-Dpython.console.encoding=UTF-8",
26 | "-Xshare:off",
27 | "-Declipse.filelock.disable=true",
28 | "-Dapple.laf.useScreenMenuBar=false",
29 | "-Dapple.awt.application.appearance=system"
30 | ]
31 | },
32 | {
33 | "type": "debugpy",
34 | "name": "PyGhidra",
35 | "request": "launch",
36 | "module": "pyghidra.ghidra_launch",
37 | "args": [
38 | "--install-dir",
39 | "${env:GHIDRA_INSTALL_DIR}",
40 | "-g",
41 | "ghidra.GhidraRun"
42 | ],
43 | "env": {
44 | "PYGHIDRA_DEBUG": "1"
45 | }
46 | },
47 | {
48 | "type": "java",
49 | "name": "Ghidra Attach",
50 | "request": "attach",
51 | "hostName": "localhost",
52 | "port": 18001
53 | }
54 | ]
55 | }
--------------------------------------------------------------------------------
/.claude/README.md:
--------------------------------------------------------------------------------
1 | # Claude Code Web Environment Setup
2 |
3 | This directory contains configuration and scripts for Claude Code Web environment.
4 |
5 | ## Files
6 |
7 | - **settings.json** - Claude Code configuration including hooks and permissions
8 | - **setup-environment.sh** - SessionStart hook script that configures the web environment
9 |
10 | ## SessionStart Hook
11 |
12 | The `setup-environment.sh` script automatically runs when a Claude Code Web session starts and:
13 |
14 | 1. **Only runs in web environments** - Skips execution on local installations
15 | 2. **Installs required dependencies**:
16 | - OpenJDK 21 (Java Development Kit)
17 | - Gradle 8.14 (Build tool)
18 | - Ghidra latest release (Reverse engineering framework)
19 | 3. **Sets up environment variables**:
20 | - `GHIDRA_INSTALL_DIR=/opt/ghidra`
21 | - `PATH` includes `/opt/gradle/bin`
22 | 4. **Persists configuration** - Saves environment variables to `CLAUDE_ENV_FILE` for subsequent bash commands
23 | 5. **Caches setup** - Uses `/tmp/.reva-env-setup-complete` marker to skip reinstallation on session resume
24 |
25 | ## Installation Locations
26 |
27 | - **Gradle**: `/opt/gradle`
28 | - **Ghidra**: `/opt/ghidra`
29 |
30 | ## Environment Detection
31 |
32 | The script uses `CLAUDE_CODE_REMOTE` environment variable to detect web environments and only runs there, ensuring it doesn't interfere with local development setups.
33 |
34 | ## Building
35 |
36 | After environment setup completes, you can build the project with:
37 |
38 | ```bash
39 | gradle buildExtension
40 | ```
41 |
42 | The environment is configured to match the CI/CD pipeline defined in `.github/workflows/test-ghidra.yml`.
43 |
--------------------------------------------------------------------------------
/data/buildLanguage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/tests/test_pyghidra.py:
--------------------------------------------------------------------------------
1 | """
2 | Test PyGhidra integration for headless Ghidra operation.
3 |
4 | Verifies that:
5 | - PyGhidra can be imported
6 | - Ghidra can be initialized in headless mode
7 | - Basic Ghidra functionality works (program creation, etc.)
8 | """
9 |
10 | import pytest
11 |
12 |
13 | class TestPyGhidraIntegration:
14 | """Test that PyGhidra integration works correctly"""
15 |
16 | def test_pyghidra_imports(self):
17 | """PyGhidra module can be imported"""
18 | import pyghidra
19 | assert pyghidra is not None
20 |
21 | def test_ghidra_initialized(self, ghidra_initialized):
22 | """Ghidra can be initialized in headless mode"""
23 | # The fixture handles initialization
24 | # Just verify we can import Ghidra classes
25 | from ghidra.program.database import ProgramDB
26 | from ghidra.program.model.lang import LanguageID
27 | assert ProgramDB is not None
28 | assert LanguageID is not None
29 |
30 | def test_test_program_created(self, test_program):
31 | """Test program fixture creates valid program"""
32 | assert test_program is not None, "Failed to create test program"
33 |
34 | # Verify program properties
35 | assert test_program.getName() == "TestHeadlessProgram"
36 |
37 | # Verify memory was created
38 | memory = test_program.getMemory()
39 | assert memory is not None
40 |
41 | # Verify .text section exists
42 | text_block = memory.getBlock(".text")
43 | assert text_block is not None
44 | assert text_block.getStart().getOffset() == 0x00401000
45 | assert text_block.getSize() == 0x1000
46 |
47 | def test_reva_classes_importable(self, ghidra_initialized):
48 | """ReVa classes can be imported after Ghidra initialization"""
49 | from reva.headless import RevaHeadlessLauncher
50 | assert RevaHeadlessLauncher is not None
51 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/plugin/RevaPluginIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import org.junit.Test;
21 |
22 | import reva.RevaIntegrationTestBase;
23 | import reva.util.RevaInternalServiceRegistry;
24 |
25 | /**
26 | * Integration tests for the RevaPlugin
27 | */
28 | public class RevaPluginIntegrationTest extends RevaIntegrationTestBase {
29 |
30 | @Test
31 | public void testPluginLoadsSuccessfully() {
32 | assertNotNull("Plugin should be loaded", plugin);
33 | assertEquals("Plugin should have correct name", "RevaPlugin", plugin.getName());
34 | }
35 |
36 | @Test
37 | public void testPluginRegistersInServiceRegistry() {
38 | RevaPlugin registeredPlugin = RevaInternalServiceRegistry.getService(RevaPlugin.class);
39 | assertNotNull("Plugin should be registered in service registry", registeredPlugin);
40 | assertEquals("Registered plugin should be the same instance", plugin, registeredPlugin);
41 | }
42 |
43 | @Test
44 | public void testProgramCreation() {
45 | assertNotNull("Test program should be created", program);
46 | assertNotNull("Program memory should exist", program.getMemory());
47 | assertTrue("Program should have memory blocks", program.getMemory().getBlocks().length > 0);
48 | }
49 | }
--------------------------------------------------------------------------------
/config/reva-headless-example.properties:
--------------------------------------------------------------------------------
1 | # ReVa Headless Configuration Example
2 | #
3 | # This file shows all available configuration options for ReVa in headless mode.
4 | # Copy this file and customize it for your needs.
5 | #
6 | # Usage:
7 | # python scripts/reva_headless_server.py --config config/reva-headless.properties
8 |
9 | # Server Configuration
10 | # Port number for the MCP server (default: 8080)
11 | reva.server.options.server.port=8080
12 |
13 | # Host interface to bind to (default: 127.0.0.1)
14 | # Use 127.0.0.1 for localhost only (recommended for security)
15 | # Use 0.0.0.0 to accept connections from any interface (use with caution!)
16 | reva.server.options.server.host=127.0.0.1
17 |
18 | # Whether the server is enabled (default: true)
19 | reva.server.options.server.enabled=true
20 |
21 | # API Key Authentication
22 | # Enable API key authentication for MCP server access (default: false)
23 | reva.server.options.api.key.authentication.enabled=false
24 |
25 | # API key required for access when authentication is enabled
26 | # Generate a secure random key for production use
27 | reva.server.options.api.key=ReVa-change-this-to-a-secure-random-key
28 |
29 | # Debug Configuration
30 | # Enable debug mode for additional logging (default: false)
31 | reva.server.options.debug.mode=false
32 |
33 | # Decompiler Configuration
34 | # Maximum number of functions before warning about decompiler search (default: 1000)
35 | reva.server.options.max.decompiler.search.functions=1000
36 |
37 | # Timeout in seconds for decompiler operations (default: 10)
38 | reva.server.options.decompiler.timeout.seconds=10
39 |
40 | # Security Notes:
41 | # 1. Keep API keys secure - never commit them to version control
42 | # 2. Use API key authentication in production environments
43 | # 3. Bind to 127.0.0.1 unless you need remote access
44 | # 4. When using 0.0.0.0, always enable API key authentication
45 | # 5. Use HTTPS reverse proxy for production deployments
46 |
47 | # Performance Notes:
48 | # 1. Higher decompiler timeout allows more complex analysis but slower responses
49 | # 2. Lower max functions limit prevents accidental full-program decompilation
50 | # 3. Debug mode adds overhead - disable in production
51 |
--------------------------------------------------------------------------------
/src/main/java/reva/util/RevaInternalServiceRegistry.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.util;
17 |
18 | import java.util.HashMap;
19 | import java.util.Map;
20 |
21 | /**
22 | * A simple service registry to allow components to locate each other at runtime.
23 | * This is a static registry that provides global access to core services.
24 | */
25 | public class RevaInternalServiceRegistry {
26 | private static final Map, Object> services = new HashMap<>();
27 |
28 | /**
29 | * Register a service implementation
30 | * @param The service type
31 | * @param serviceClass The service interface class
32 | * @param implementation The service implementation
33 | */
34 | public static void registerService(Class serviceClass, T implementation) {
35 | services.put(serviceClass, implementation);
36 | }
37 |
38 | /**
39 | * Get a registered service
40 | * @param The service type
41 | * @param serviceClass The service interface class
42 | * @return The service implementation or null if not found
43 | */
44 | @SuppressWarnings("unchecked")
45 | public static T getService(Class serviceClass) {
46 | return (T) services.get(serviceClass);
47 | }
48 |
49 | /**
50 | * Remove a service from the registry
51 | * @param The service type
52 | * @param serviceClass The service interface class
53 | */
54 | public static void unregisterService(Class serviceClass) {
55 | services.remove(serviceClass);
56 | }
57 |
58 | /**
59 | * Clear all registered services
60 | */
61 | public static void clearAllServices() {
62 | services.clear();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/reva/revaAnalyzer.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva;
17 |
18 | import ghidra.app.services.AbstractAnalyzer;
19 | import ghidra.app.services.AnalyzerType;
20 | import ghidra.app.util.importer.MessageLog;
21 | import ghidra.framework.options.Options;
22 | import ghidra.program.model.address.AddressSetView;
23 | import ghidra.program.model.listing.Program;
24 | import ghidra.util.exception.CancelledException;
25 | import ghidra.util.task.TaskMonitor;
26 |
27 | /**
28 | * Provide class-level documentation that describes what this analyzer does.
29 | */
30 | public class revaAnalyzer extends AbstractAnalyzer {
31 |
32 | public revaAnalyzer() {
33 |
34 | // Name the analyzer and give it a description.
35 |
36 | super("My Analyzer", "Analyzer description goes here", AnalyzerType.BYTE_ANALYZER);
37 | }
38 |
39 | @Override
40 | public boolean getDefaultEnablement(Program program) {
41 |
42 | // Return true if analyzer should be enabled by default
43 |
44 | return true;
45 | }
46 |
47 | @Override
48 | public boolean canAnalyze(Program program) {
49 |
50 | // Examine 'program' to determine of this analyzer should analyze it. Return true
51 | // if it can.
52 |
53 | return true;
54 | }
55 |
56 | @Override
57 | public void registerOptions(Options options, Program program) {
58 |
59 | // If this analyzer has custom options, register them here
60 |
61 | options.registerOption("Option name goes here", false, null,
62 | "Option description goes here");
63 | }
64 |
65 | @Override
66 | public boolean added(Program program, AddressSetView set, TaskMonitor monitor, MessageLog log)
67 | throws CancelledException {
68 |
69 | // Perform analysis when things get added to the 'program'. Return true if the
70 | // analysis succeeded.
71 |
72 | return false;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/reva/revaExporter.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva;
17 |
18 | import java.io.File;
19 | import java.io.IOException;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | import ghidra.app.util.*;
24 | import ghidra.app.util.exporter.Exporter;
25 | import ghidra.app.util.exporter.ExporterException;
26 | import ghidra.framework.model.DomainObject;
27 | import ghidra.program.model.address.AddressSetView;
28 | import ghidra.util.task.TaskMonitor;
29 |
30 | /**
31 | * Provide class-level documentation that describes what this exporter does.
32 | */
33 | public class revaExporter extends Exporter {
34 |
35 | /**
36 | * Exporter constructor.
37 | */
38 | public revaExporter() {
39 |
40 | // Name the exporter and associate a file extension with it
41 |
42 | super("My Exporter", "exp", null);
43 | }
44 |
45 | @Override
46 | public boolean supportsAddressRestrictedExport() {
47 |
48 | // Return true if addrSet export parameter can be used to restrict export
49 |
50 | return false;
51 | }
52 |
53 | @Override
54 | public boolean export(File file, DomainObject domainObj, AddressSetView addrSet,
55 | TaskMonitor monitor) throws ExporterException, IOException {
56 |
57 | // Perform the export, and return true if it succeeded
58 |
59 | return false;
60 | }
61 |
62 | @Override
63 | public List getOptions(DomainObjectService domainObjectService) {
64 | List list = new ArrayList<>();
65 |
66 | // If this exporter has custom options, add them to 'list'
67 | list.add(new Option("Option name goes here", "Default option value goes here"));
68 |
69 | return list;
70 | }
71 |
72 | @Override
73 | public void setOptions(List options) throws OptionException {
74 |
75 | // If this exporter has custom options, assign their values to the exporter here
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/reva/resources/AbstractResourceProvider.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.resources;
17 |
18 | import ghidra.program.model.listing.Program;
19 | import ghidra.util.Msg;
20 | import io.modelcontextprotocol.server.McpSyncServer;
21 |
22 | /**
23 | * Base implementation of the ResourceProvider interface.
24 | * Provides common functionality for all resource providers.
25 | */
26 | public abstract class AbstractResourceProvider implements ResourceProvider {
27 | protected final McpSyncServer server;
28 |
29 | /**
30 | * Constructor
31 | * @param server The MCP server to register resources with
32 | */
33 | public AbstractResourceProvider(McpSyncServer server) {
34 | this.server = server;
35 | }
36 |
37 | @Override
38 | public void programOpened(Program program) {
39 | // Default implementation does nothing
40 | }
41 |
42 | @Override
43 | public void programClosed(Program program) {
44 | // Default implementation does nothing
45 | }
46 |
47 | @Override
48 | public void cleanup() {
49 | // Default implementation does nothing
50 | }
51 |
52 | /**
53 | * Log an error message
54 | * @param message The message to log
55 | */
56 | protected void logError(String message) {
57 | Msg.error(this, message);
58 | }
59 |
60 | /**
61 | * Log an error message with an exception
62 | * @param message The message to log
63 | * @param e The exception that caused the error
64 | */
65 | protected void logError(String message, Exception e) {
66 | Msg.error(this, message, e);
67 | }
68 |
69 | /**
70 | * Log an informational message
71 | * @param message The message to log
72 | */
73 | protected void logInfo(String message) {
74 | Msg.info(this, message);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # Testing Guidelines
2 | - Integration tests go in `test.slow`, unit tests in `test`
3 | - Unit tests should test things that don't require a Ghidra environment
4 | - **CRITICAL**: Integration tests should validate actual Ghidra program state changes, not just MCP tool responses
5 | - Use `Function.getParameters()` and `Function.getAllVariables()` to validate variable changes
6 | - Use `DataType.isEquivalent()` to compare datatypes before/after changes
7 | - **You are not finished until all the tests pass!**
8 |
9 | # Address Formatting
10 | - **ALWAYS use `AddressUtil.formatAddress(address)`** for consistent address formatting in JSON output
11 | - This ensures all addresses have the "0x" prefix format consistently across all ReVa tools
12 | - Import: `import reva.util.AddressUtil;`
13 | - Format: `AddressUtil.formatAddress(address)` returns `"0x" + address.toString()`
14 |
15 | # Decompiler Tool Implementation Pattern
16 | ## Adding New Tools to DecompilerToolProvider.java
17 | 1. Create `register[ToolName]Tool()` method following existing patterns
18 | 2. Call it from `registerTools()` method
19 | 3. Use `HighFunctionDBUtil.updateDBVariable()` for persisting variable changes
20 | 4. Follow the `rename-variables` pattern for consistency
21 | 5. Handle decompilation with proper error handling and transaction management
22 |
23 | ## Key APIs
24 | - `DataTypeParserUtil.parseDataTypeObjectFromString()` - parse datatype strings like "char*", "int[10]"
25 | - `HighFunctionDBUtil.updateDBVariable(symbol, newName, newDataType, SourceType.USER_DEFINED)` - persist changes
26 | - `DecompInterface` - get decompiled function and high-level representation
27 | - `LocalSymbolMap.getSymbols()` returns Iterator, not Iterable - use while loop, not for-each
28 |
29 | ## Common Patterns
30 | - Always use transactions when modifying program state
31 | - Handle decompilation failures gracefully with try-catch
32 | - Validate parameters before processing (non-empty mappings, valid function, etc.)
33 | - Return structured JSON with success flags and updated decompilation
34 | - Follow MCP tool schema patterns for consistency
35 | - Use AbstractToolProvider helper methods (getString, getInt, getOptionalInt, getOptionalBoolean) for all parameters to handle type conversion and validation
36 | - Wrap parameter extraction in try-catch blocks to convert IllegalArgumentException to user-friendly createErrorResult calls
37 |
38 | # JUnit Version
39 | - Use JUnit 4 for all tests (imports: `org.junit.Test`, `org.junit.Before`)
40 | - Avoid JUnit 5 annotations (`@ParameterizedTest`, etc.) - causes compilation errors
41 |
--------------------------------------------------------------------------------
/src/main/java/reva/services/RevaMcpService.java:
--------------------------------------------------------------------------------
1 | package reva.services;
2 |
3 | import ghidra.framework.plugintool.PluginTool;
4 | import ghidra.program.model.listing.Program;
5 |
6 | /**
7 | * Service interface for accessing the application-level MCP server.
8 | * This service is provided by the RevaApplicationPlugin and consumed by tool-level plugins
9 | * to register with the shared MCP server that persists across tool sessions.
10 | */
11 | public interface RevaMcpService {
12 |
13 | /**
14 | * Register a tool with the MCP server.
15 | * This allows the tool to receive program lifecycle notifications
16 | * and participate in MCP server operations.
17 | *
18 | * @param tool The tool to register
19 | */
20 | void registerTool(PluginTool tool);
21 |
22 | /**
23 | * Unregister a tool from the MCP server.
24 | * Called when a tool is closing or no longer needs MCP services.
25 | *
26 | * @param tool The tool to unregister
27 | */
28 | void unregisterTool(PluginTool tool);
29 |
30 | /**
31 | * Notify the MCP server that a program has been opened in a tool.
32 | * The server will track which programs are open in which tools.
33 | *
34 | * @param program The program that was opened
35 | * @param tool The tool where the program was opened
36 | */
37 | void programOpened(Program program, PluginTool tool);
38 |
39 | /**
40 | * Notify the MCP server that a program has been closed in a tool.
41 | * The server will update its tracking of program-to-tool mappings.
42 | *
43 | * @param program The program that was closed
44 | * @param tool The tool where the program was closed
45 | */
46 | void programClosed(Program program, PluginTool tool);
47 |
48 | /**
49 | * Check if the MCP server is currently running and accepting connections.
50 | *
51 | * @return true if the server is running, false otherwise
52 | */
53 | boolean isServerRunning();
54 |
55 | /**
56 | * Get the port number the MCP server is listening on.
57 | *
58 | * @return The server port number, or -1 if server is not running
59 | */
60 | int getServerPort();
61 |
62 | /**
63 | * Get the currently active program for MCP operations.
64 | * This is typically the program that was most recently opened
65 | * or the one in the currently focused tool.
66 | *
67 | * @return The active program, or null if no program is active
68 | */
69 | Program getActiveProgram();
70 |
71 | /**
72 | * Set the active program for MCP operations.
73 | * This is typically called when focus changes between tools.
74 | *
75 | * @param program The program to set as active
76 | * @param tool The tool containing the active program
77 | */
78 | void setActiveProgram(Program program, PluginTool tool);
79 | }
--------------------------------------------------------------------------------
/src/test/java/reva/RevaHeadlessIntegrationTestBase.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva;
17 |
18 | import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
19 | import ghidra.program.model.listing.Program;
20 | import ghidra.program.database.ProgramDB;
21 | import ghidra.program.model.lang.Language;
22 | import ghidra.program.model.lang.LanguageID;
23 | import ghidra.program.model.lang.CompilerSpec;
24 | import ghidra.program.model.mem.Memory;
25 | import ghidra.util.task.TaskMonitor;
26 |
27 | import org.junit.After;
28 | import org.junit.Before;
29 |
30 | /**
31 | * Base class for ReVa headless integration tests that don't require a GUI tool.
32 | * This is a simpler alternative that just tests with programs directly.
33 | */
34 | public abstract class RevaHeadlessIntegrationTestBase extends AbstractGhidraHeadlessIntegrationTest {
35 |
36 | protected Program program;
37 |
38 | @Before
39 | public void setUp() throws Exception {
40 | // Create a test program
41 | program = createDefaultProgram();
42 | }
43 |
44 | @After
45 | public void tearDown() throws Exception {
46 | if (program != null && program instanceof ProgramDB) {
47 | ((ProgramDB) program).release(this);
48 | }
49 | program = null;
50 | }
51 |
52 | /**
53 | * Creates a default program for testing.
54 | * Subclasses can override this to customize the test program.
55 | *
56 | * @return A new Program instance
57 | * @throws Exception if program creation fails
58 | */
59 | protected Program createDefaultProgram() throws Exception {
60 | Language language = getLanguageService().getLanguage(new LanguageID("x86:LE:32:default"));
61 | CompilerSpec compilerSpec = language.getDefaultCompilerSpec();
62 | ProgramDB testProgram = new ProgramDB("TestProgram", language, compilerSpec, this);
63 |
64 | // Add a memory block
65 | Memory memory = testProgram.getMemory();
66 | int txId = testProgram.startTransaction("Create Memory");
67 | try {
68 | memory.createInitializedBlock("test",
69 | testProgram.getAddressFactory().getDefaultAddressSpace().getAddress(0x01000000),
70 | 0x1000, (byte) 0, TaskMonitor.DUMMY, false);
71 | } finally {
72 | testProgram.endTransaction(txId, true);
73 | }
74 |
75 | return testProgram;
76 | }
77 | }
--------------------------------------------------------------------------------
/src/main/help/help/TOC_Source.xml:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
52 |
57 |
58 |
--------------------------------------------------------------------------------
/.github/workflows/test-headless.yml:
--------------------------------------------------------------------------------
1 | name: Test Headless Mode 🤖
2 |
3 | on:
4 | push:
5 | branches: [ main, develop ]
6 | pull_request:
7 | branches: [ main, develop ]
8 | workflow_dispatch: # Allow manual trigger
9 |
10 | permissions:
11 | contents: read
12 | actions: read
13 | checks: write
14 |
15 | jobs:
16 | test-headless:
17 | strategy:
18 | matrix:
19 | os: [ubuntu-latest, macos-latest]
20 | ghidra-version: ["12.0", "latest"]
21 | fail-fast: false # Continue other tests if one fails
22 |
23 | name: Test on ${{ matrix.os }} / Ghidra ${{ matrix.ghidra-version }}
24 | runs-on: ${{ matrix.os }}
25 |
26 | steps:
27 | - name: Checkout code
28 | uses: actions/checkout@v5
29 |
30 | - name: Set up Java 21
31 | uses: actions/setup-java@v5
32 | with:
33 | java-version: "21"
34 | distribution: "microsoft"
35 |
36 | - name: Set up Python 3.10
37 | uses: actions/setup-python@v6
38 | with:
39 | python-version: "3.10"
40 |
41 | - name: Install Ghidra ${{ matrix.ghidra-version }}
42 | uses: antoniovazquezblanco/setup-ghidra@v2.0.15
43 | with:
44 | version: ${{ matrix.ghidra-version }}
45 |
46 | - name: Setup Gradle
47 | uses: gradle/actions/setup-gradle@v5
48 | with:
49 | gradle-version: "8.14"
50 |
51 | - name: Build ReVa Extension
52 | run: gradle clean buildExtension --no-build-cache
53 |
54 | - name: Install ReVa Extension to Ghidra
55 | run: |
56 | EXTENSION_ZIP=$(ls -1 dist/*.zip | head -n 1)
57 | echo "Installing extension: $EXTENSION_ZIP"
58 | unzip -q "$EXTENSION_ZIP" -d "$GHIDRA_INSTALL_DIR/Ghidra/Extensions/"
59 | ls -la "$GHIDRA_INSTALL_DIR/Ghidra/Extensions/"
60 |
61 | - name: Install uv
62 | uses: astral-sh/setup-uv@v5
63 | with:
64 | enable-cache: true
65 |
66 | - name: Install Python dependencies
67 | run: uv sync --prerelease=allow
68 |
69 | - name: Install PyGhidra from local Ghidra
70 | run: |
71 | echo "GHIDRA_INSTALL_DIR: $GHIDRA_INSTALL_DIR"
72 | uv pip install "${GHIDRA_INSTALL_DIR}/Ghidra/Features/PyGhidra/pypkg"
73 |
74 | - name: Verify PyGhidra Installation
75 | run: |
76 | uv run python -c "import pyghidra; print(f'PyGhidra version: {pyghidra.__version__}')"
77 | echo "PyGhidra installed successfully"
78 |
79 | - name: Run Headless Integration Tests
80 | id: headless_test
81 | run: |
82 | echo "::group::Running headless integration tests"
83 | uv run pytest tests/ -v --timeout=180 --tb=short --junitxml=test-results.xml
84 | echo "::endgroup::"
85 | timeout-minutes: 30
86 | env:
87 | GHIDRA_INSTALL_DIR: ${{ env.GHIDRA_INSTALL_DIR }}
88 |
89 | - name: Upload test artifacts
90 | uses: actions/upload-artifact@v5
91 | if: always()
92 | with:
93 | name: headless-test-results-${{ matrix.os }}-ghidra-${{ matrix.ghidra-version }}
94 | path: |
95 | test-results.xml
96 | *.log
97 | .pytest_cache/
98 | if-no-files-found: ignore
99 |
--------------------------------------------------------------------------------
/src/main/java/reva/util/SimilarityComparator.java:
--------------------------------------------------------------------------------
1 | package reva.util;
2 |
3 |
4 | public class SimilarityComparator implements java.util.Comparator {
5 |
6 | public static abstract class StringExtractor {
7 | public abstract String extract(T item);
8 | }
9 |
10 | private final String searchString;
11 | private final StringExtractor extractor;
12 |
13 | public SimilarityComparator(String searchString, StringExtractor extractor) {
14 | this.searchString = searchString.toLowerCase();
15 | this.extractor = extractor;
16 | }
17 |
18 | @Override
19 | public int compare(T o1, T o2) {
20 | String str1 = extractor.extract(o1);
21 | String str2 = extractor.extract(o2);
22 | // Handle null values - treat as empty strings for comparison
23 | str1 = str1 != null ? str1.toLowerCase() : "";
24 | str2 = str2 != null ? str2.toLowerCase() : "";
25 | return findLongestCommonSubstringLength(str2, searchString) -
26 | findLongestCommonSubstringLength(str1, searchString);
27 | }
28 |
29 | private int findLongestCommonSubstringLength(String str1, String str2) {
30 | return lcsLength(str1, str2);
31 | }
32 |
33 | /**
34 | * Calculate the longest common substring length between two strings.
35 | * @param str1 First string
36 | * @param str2 Second string
37 | * @return Length of the longest common substring
38 | */
39 | private static int lcsLength(String str1, String str2) {
40 | int m = str1.length();
41 | int n = str2.length();
42 | int[][] dp = new int[m + 1][n + 1];
43 | int maxLength = 0;
44 |
45 | for (int i = 1; i <= m; i++) {
46 | for (int j = 1; j <= n; j++) {
47 | if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
48 | dp[i][j] = 1 + dp[i - 1][j - 1];
49 | if (dp[i][j] > maxLength) {
50 | maxLength = dp[i][j];
51 | }
52 | } else {
53 | dp[i][j] = 0; // Reset if characters don't match
54 | }
55 | }
56 | }
57 | return maxLength;
58 | }
59 |
60 | /**
61 | * Calculate the LCS-based similarity score between two strings.
62 | * Returns a value between 0.0 and 1.0, where 1.0 means the longest common
63 | * substring equals the length of the shorter string (i.e., the shorter
64 | * string appears as a contiguous substring in the longer string).
65 | * @param str1 First string (should be lowercase for case-insensitive comparison)
66 | * @param str2 Second string (should be lowercase for case-insensitive comparison)
67 | * @return Similarity score between 0.0 and 1.0
68 | */
69 | public static double calculateLcsSimilarity(String str1, String str2) {
70 | if (str1 == null || str2 == null || str1.isEmpty() || str2.isEmpty()) {
71 | return 0.0;
72 | }
73 | int lcsLen = lcsLength(str1, str2);
74 | int minLen = Math.min(str1.length(), str2.length());
75 | return (double) lcsLen / minLen;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/.github/workflows/publish-ghidra.yml:
--------------------------------------------------------------------------------
1 | name: Publish Ghidra 🐉 distribution
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | workflow_dispatch:
7 | inputs:
8 | release_tag:
9 | description: 'Release tag to update (e.g., v4.1.0). If not provided, uses the current ref.'
10 | required: false
11 | type: string
12 | update_existing:
13 | description: 'Update existing release instead of creating new one'
14 | required: false
15 | type: boolean
16 | default: true
17 | jobs:
18 | build:
19 | permissions:
20 | contents: read
21 | strategy:
22 | matrix:
23 | # Build only on Linux since the extension is Java-only and platform-independent
24 | # This simplifies the build process and reduces CI time
25 | ghidra-version: [
26 | "12.0",
27 | "latest",
28 | ]
29 | name: Build distribution 📦
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v5
33 | - name: Set up Java 🍵
34 | uses: actions/setup-java@v5
35 | with:
36 | java-version: "21"
37 | distribution: "microsoft"
38 | - name: Install Ghidra 🐉
39 | uses: antoniovazquezblanco/setup-ghidra@v2.0.15
40 | with:
41 | version: ${{ matrix.ghidra-version }}
42 | - name: Compile ReVa 👩💻✨
43 | run: gradle buildExtension
44 | - name: Store the distribution packages
45 | uses: actions/upload-artifact@v5
46 | with:
47 | name: ghidra-distributions-${{ matrix.ghidra-version }}
48 | path: dist/
49 | if-no-files-found: error
50 | github-release:
51 | name: >-
52 | Sign the Ghidra 🐉 distribution 📦 with Sigstore
53 | and upload them to GitHub Release
54 | needs:
55 | - build
56 | runs-on: ubuntu-latest
57 | permissions:
58 | contents: write # IMPORTANT: mandatory for making GitHub Releases
59 | id-token: write # IMPORTANT: mandatory for sigstore
60 | env:
61 | RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
62 | steps:
63 | - name: Download all the dists
64 | uses: actions/download-artifact@v6
65 | with:
66 | pattern: ghidra-distributions-*
67 | path: dist/
68 | merge-multiple: true
69 |
70 | - name: Sign the dists with Sigstore
71 | uses: sigstore/gh-action-sigstore-python@v3.1.0
72 | with:
73 | inputs: >-
74 | ./dist/*.zip
75 | upload-signing-artifacts: true
76 |
77 | - name: Create or update GitHub Release
78 | env:
79 | GITHUB_TOKEN: ${{ github.token }}
80 | run: |
81 | if [[ "${{ inputs.update_existing }}" == "true" ]] && gh release view "$RELEASE_TAG" --repo '${{ github.repository }}' >/dev/null 2>&1; then
82 | echo "Release $RELEASE_TAG already exists, will update it"
83 | else
84 | echo "Creating new release $RELEASE_TAG"
85 | gh release create "$RELEASE_TAG" \
86 | --repo '${{ github.repository }}' \
87 | --notes-from-tag
88 | fi
89 |
90 | - name: Upload build artifacts and signatures to GitHub Release
91 | env:
92 | GITHUB_TOKEN: ${{ github.token }}
93 | run: >-
94 | gh release upload
95 | "$RELEASE_TAG" ./dist/* --clobber
96 | --repo '${{ github.repository }}'
--------------------------------------------------------------------------------
/src/test/java/reva/plugin/RevaPluginUnitTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import org.junit.Test;
21 |
22 | import ghidra.app.plugin.PluginCategoryNames;
23 | import ghidra.framework.plugintool.util.PluginStatus;
24 | import ghidra.framework.plugintool.PluginInfo;
25 |
26 | /**
27 | * Unit tests for RevaPlugin metadata and structure
28 | */
29 | public class RevaPluginUnitTest {
30 |
31 | @Test
32 | public void testPluginAnnotation() {
33 | // Get the plugin info annotation
34 | PluginInfo info = RevaPlugin.class.getAnnotation(PluginInfo.class);
35 |
36 | assertNotNull("Plugin should have @PluginInfo annotation", info);
37 | assertEquals("Plugin status should be RELEASED", PluginStatus.RELEASED, info.status());
38 | assertEquals("Plugin package name should be ReVa", "ReVa", info.packageName());
39 | assertEquals("Plugin category should be COMMON", PluginCategoryNames.COMMON, info.category());
40 | assertEquals("Plugin short description should match",
41 | "Reverse Engineering Assistant (Tool)", info.shortDescription());
42 | assertEquals("Plugin description should match",
43 | "Tool-level ReVa plugin that connects to the application-level MCP server",
44 | info.description());
45 | }
46 |
47 | @Test
48 | public void testPluginInheritance() {
49 | // Verify the plugin extends the correct base class
50 | assertTrue("RevaPlugin should extend ProgramPlugin",
51 | ghidra.app.plugin.ProgramPlugin.class.isAssignableFrom(RevaPlugin.class));
52 | }
53 |
54 | @Test
55 | public void testPluginMethods() throws NoSuchMethodException {
56 | // Check for required method overrides
57 | assertNotNull("Should have init method",
58 | RevaPlugin.class.getDeclaredMethod("init"));
59 |
60 | assertNotNull("Should have cleanup method",
61 | RevaPlugin.class.getDeclaredMethod("cleanup"));
62 |
63 | assertNotNull("Should have programOpened method",
64 | RevaPlugin.class.getDeclaredMethod("programOpened", ghidra.program.model.listing.Program.class));
65 |
66 | assertNotNull("Should have programClosed method",
67 | RevaPlugin.class.getDeclaredMethod("programClosed", ghidra.program.model.listing.Program.class));
68 | }
69 |
70 | @Test
71 | public void testPluginFields() throws NoSuchFieldException {
72 | // Check for expected fields
73 | assertNotNull("Should have provider field",
74 | RevaPlugin.class.getDeclaredField("provider"));
75 |
76 | assertNotNull("Should have mcpService field",
77 | RevaPlugin.class.getDeclaredField("mcpService"));
78 | }
79 | }
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "reverse-engineering-assistant"
3 | dynamic = ["version"]
4 | description = "ReVa - AI-powered Reverse Engineering Assistant for Ghidra with MCP server and Claude CLI integration"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | license = {text = "Apache-2.0"}
8 | authors = [
9 | {name = "cyberkaida", email = "cyberkaida@users.noreply.github.com"}
10 | ]
11 | maintainers = [
12 | {name = "cyberkaida"}
13 | ]
14 | keywords = [
15 | "reverse-engineering",
16 | "ghidra",
17 | "mcp",
18 | "ai",
19 | "disassembly",
20 | "decompiler",
21 | "binary-analysis",
22 | "claude",
23 | "security-research"
24 | ]
25 | classifiers = [
26 | "Development Status :: 4 - Beta",
27 | "Intended Audience :: Developers",
28 | "Intended Audience :: Science/Research",
29 | "License :: OSI Approved :: Apache Software License",
30 | "Natural Language :: English",
31 | "Operating System :: OS Independent",
32 | "Programming Language :: Python :: 3",
33 | "Programming Language :: Python :: 3.10",
34 | "Programming Language :: Python :: 3.11",
35 | "Programming Language :: Python :: 3.12",
36 | "Programming Language :: Java",
37 | "Topic :: Security",
38 | "Topic :: Software Development :: Disassemblers",
39 | "Topic :: Scientific/Engineering :: Information Analysis",
40 | ]
41 |
42 | dependencies = [
43 | "pyghidra>=3.0.0",
44 | "mcp",
45 | "httpx>=0.27.0,<1.0",
46 | "httpx-sse>=0.4.0",
47 | ]
48 |
49 | [project.urls]
50 | Homepage = "https://github.com/cyberkaida/reverse-engineering-assistant"
51 | Documentation = "https://github.com/cyberkaida/reverse-engineering-assistant/tree/main/docs"
52 | Repository = "https://github.com/cyberkaida/reverse-engineering-assistant"
53 | Issues = "https://github.com/cyberkaida/reverse-engineering-assistant/issues"
54 | Changelog = "https://github.com/cyberkaida/reverse-engineering-assistant/releases"
55 |
56 | [project.scripts]
57 | mcp-reva = "reva_cli.__main__:main"
58 |
59 | [project.optional-dependencies]
60 | test = [
61 | "pytest>=7.0.0",
62 | "pytest-asyncio>=0.21.0",
63 | "pytest-timeout>=2.0.0",
64 | "pytest-sugar>=0.9.0",
65 | "mcp",
66 | "httpx>=0.27.0,<1.0",
67 | "httpx-sse>=0.4.0",
68 | ]
69 |
70 | [build-system]
71 | requires = ["setuptools>=45", "setuptools_scm>=8.0", "wheel"]
72 | build-backend = "setuptools.build_meta"
73 |
74 | [tool.setuptools]
75 | packages = ["reva_cli"]
76 | package-dir = {"" = "src"}
77 |
78 | [tool.pytest.ini_options]
79 | testpaths = ["tests"]
80 | python_files = ["test_*.py"]
81 | python_classes = ["Test*"]
82 | python_functions = ["test_*"]
83 | timeout = 60
84 | addopts = "-v --tb=short"
85 | markers = [
86 | "unit: Fast unit tests with mocked dependencies",
87 | "integration: Integration tests with real PyGhidra",
88 | "e2e: End-to-end tests via subprocess",
89 | "cli: All CLI-specific tests",
90 | "slow: Tests that take more than 5 seconds"
91 | ]
92 |
93 | [dependency-groups]
94 | dev = [
95 | "pytest>=7.0.0",
96 | "pytest-asyncio>=0.21.0",
97 | "pytest-timeout>=2.0.0",
98 | "pytest-sugar>=0.9.0",
99 | "mcp",
100 | "httpx>=0.27.0,<1.0",
101 | "httpx-sse>=0.4.0",
102 | ]
103 |
104 | [tool.setuptools_scm]
105 | # Extract version from git tags (e.g., v5.0.0 -> 5.0.0)
106 | # Fallback to 0.0.0.dev0 if no tags found
107 | version_file = "src/reva_cli/_version.py"
108 |
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/config/ConfigurationBackend.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin.config;
17 |
18 | /**
19 | * Backend interface for configuration storage.
20 | * This abstraction allows ConfigManager to work with different storage
21 | * mechanisms: ToolOptions (GUI mode), files (headless mode), or in-memory (testing).
22 | */
23 | public interface ConfigurationBackend {
24 |
25 | /**
26 | * Get an integer configuration value
27 | * @param category The configuration category
28 | * @param name The configuration name
29 | * @param defaultValue The default value if not found
30 | * @return The configured value or default
31 | */
32 | int getInt(String category, String name, int defaultValue);
33 |
34 | /**
35 | * Set an integer configuration value
36 | * @param category The configuration category
37 | * @param name The configuration name
38 | * @param value The value to set
39 | */
40 | void setInt(String category, String name, int value);
41 |
42 | /**
43 | * Get a string configuration value
44 | * @param category The configuration category
45 | * @param name The configuration name
46 | * @param defaultValue The default value if not found
47 | * @return The configured value or default
48 | */
49 | String getString(String category, String name, String defaultValue);
50 |
51 | /**
52 | * Set a string configuration value
53 | * @param category The configuration category
54 | * @param name The configuration name
55 | * @param value The value to set
56 | */
57 | void setString(String category, String name, String value);
58 |
59 | /**
60 | * Get a boolean configuration value
61 | * @param category The configuration category
62 | * @param name The configuration name
63 | * @param defaultValue The default value if not found
64 | * @return The configured value or default
65 | */
66 | boolean getBoolean(String category, String name, boolean defaultValue);
67 |
68 | /**
69 | * Set a boolean configuration value
70 | * @param category The configuration category
71 | * @param name The configuration name
72 | * @param value The value to set
73 | */
74 | void setBoolean(String category, String name, boolean value);
75 |
76 | /**
77 | * Check if this backend supports change notifications
78 | * @return True if this backend can notify listeners of changes
79 | */
80 | boolean supportsChangeNotifications();
81 |
82 | /**
83 | * Register a listener for configuration changes (if supported)
84 | * @param listener The listener to register
85 | */
86 | void addChangeListener(ConfigurationBackendListener listener);
87 |
88 | /**
89 | * Unregister a configuration change listener
90 | * @param listener The listener to remove
91 | */
92 | void removeChangeListener(ConfigurationBackendListener listener);
93 |
94 | /**
95 | * Clean up resources when done with this backend
96 | */
97 | void dispose();
98 | }
99 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/tools/memory/MemoryToolProviderIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools.memory;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import org.junit.Before;
21 | import org.junit.Test;
22 |
23 | import ghidra.program.model.mem.MemoryBlock;
24 | import reva.RevaIntegrationTestBase;
25 |
26 | /**
27 | * Integration tests for MemoryToolProvider that verify MCP tool registration
28 | * and basic functionality.
29 | */
30 | public class MemoryToolProviderIntegrationTest extends RevaIntegrationTestBase {
31 |
32 | private String programPath;
33 |
34 | @Before
35 | public void setUpTestData() throws Exception {
36 | programPath = program.getDomainFile().getPathname();
37 | }
38 |
39 | @Test
40 | public void testMemoryBlocksAndToolRegistration() throws Exception {
41 | // Verify that the program has the expected memory block
42 | MemoryBlock[] blocks = program.getMemory().getBlocks();
43 | assertTrue("Program should have at least one memory block", blocks.length > 0);
44 |
45 | // Find the test memory block
46 | MemoryBlock testBlock = null;
47 | for (MemoryBlock block : blocks) {
48 | if ("test".equals(block.getName())) {
49 | testBlock = block;
50 | break;
51 | }
52 | }
53 | assertNotNull("Test memory block should exist", testBlock);
54 | assertEquals("Test block should start at 0x01000000",
55 | 0x01000000L, testBlock.getStart().getOffset());
56 | assertEquals("Test block should be 0x1000 bytes", 0x1000, testBlock.getSize());
57 |
58 | // Verify that the MCP server has the MemoryToolProvider tools registered
59 | // We can check this by looking at the server's registered tools
60 | io.modelcontextprotocol.server.McpSyncServer mcpServer =
61 | reva.util.RevaInternalServiceRegistry.getService(io.modelcontextprotocol.server.McpSyncServer.class);
62 | assertNotNull("MCP server should be registered", mcpServer);
63 |
64 | // The memory tools should be registered: get-memory-blocks, read-memory
65 | // This validates that our tool provider integration is working
66 | }
67 |
68 | @Test
69 | public void testProgramSetupForMemoryTests() throws Exception {
70 | // Verify that the program path is set correctly
71 | assertNotNull("Program path should be set", programPath);
72 | assertNotNull("Program should be set", program);
73 |
74 | // Verify the config manager and server port are available
75 | assertNotNull("Config manager should be available", configManager);
76 | assertEquals("Server port should be 8080", 8080, configManager.getServerPort());
77 |
78 | // Verify that we have a usable memory space
79 | assertNotNull("Program should have memory", program.getMemory());
80 | assertTrue("Program should have at least one memory block",
81 | program.getMemory().getBlocks().length > 0);
82 | }
83 | }
--------------------------------------------------------------------------------
/src/main/java/reva/ui/RevaProvider.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.ui;
17 |
18 | import java.awt.BorderLayout;
19 | import javax.swing.*;
20 |
21 | import docking.ActionContext;
22 | import docking.ComponentProvider;
23 | import docking.action.DockingAction;
24 | import docking.action.ToolBarData;
25 | import ghidra.framework.plugintool.Plugin;
26 | import ghidra.util.HelpLocation;
27 | import resources.Icons;
28 |
29 | /**
30 | * UI provider for the ReVa plugin.
31 | * This class provides the graphical user interface for configuring and
32 | * monitoring the ReVa server.
33 | */
34 | public class RevaProvider extends ComponentProvider {
35 | private JPanel panel;
36 | private JTextArea statusArea;
37 | private DockingAction configAction;
38 |
39 | /**
40 | * Constructor
41 | * @param plugin The parent plugin
42 | * @param owner The owner name
43 | */
44 | public RevaProvider(Plugin plugin, String owner) {
45 | super(plugin.getTool(), "ReVa Provider", owner);
46 | buildPanel();
47 | createActions();
48 | setStatusText("ReVa Model Context Protocol server is running");
49 | }
50 |
51 | /**
52 | * Build the UI panel
53 | */
54 | private void buildPanel() {
55 | panel = new JPanel(new BorderLayout());
56 |
57 | // Status area to show server status
58 | statusArea = new JTextArea(10, 40);
59 | statusArea.setEditable(false);
60 |
61 | // Add components to panel
62 | panel.add(new JScrollPane(statusArea), BorderLayout.CENTER);
63 |
64 | setVisible(true);
65 | }
66 |
67 | /**
68 | * Create actions for the toolbar
69 | */
70 | private void createActions() {
71 | configAction = new DockingAction("ReVa Configuration", getName()) {
72 | @Override
73 | public void actionPerformed(ActionContext context) {
74 | // TODO: Show configuration dialog
75 | JOptionPane.showMessageDialog(panel,
76 | "ReVa Configuration (TODO)",
77 | "ReVa Configuration",
78 | JOptionPane.INFORMATION_MESSAGE);
79 | }
80 | };
81 |
82 | configAction.setToolBarData(new ToolBarData(Icons.HELP_ICON, null));
83 | configAction.setEnabled(true);
84 | configAction.setDescription("Configure ReVa");
85 | configAction.setHelpLocation(new HelpLocation("ReVa", "Configuration"));
86 |
87 | addLocalAction(configAction);
88 | }
89 |
90 | /**
91 | * Set status text to display
92 | * @param status The status text to display
93 | */
94 | public void setStatusText(String status) {
95 | statusArea.append(status + "\n");
96 | statusArea.setCaretPosition(statusArea.getText().length());
97 | }
98 |
99 | /**
100 | * Get the UI component
101 | * @return The JComponent for this provider
102 | */
103 | @Override
104 | public JComponent getComponent() {
105 | return panel;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Test ReVa configuration file loading and options.
3 |
4 | Verifies that:
5 | - Configuration files can be loaded
6 | - Options are applied correctly
7 | - Invalid configs are handled gracefully
8 | """
9 |
10 | import pytest
11 | from pathlib import Path
12 |
13 |
14 | class TestConfigurationLoading:
15 | """Test configuration file loading"""
16 |
17 | def test_default_configuration(self, ghidra_initialized):
18 | """Launcher works with default in-memory configuration"""
19 | from reva.headless import RevaHeadlessLauncher
20 | from reva.plugin import ConfigManager
21 |
22 | # Create default config manager
23 | config = ConfigManager()
24 |
25 | # Should have default port
26 | port = config.getPort()
27 | assert port == 8080
28 |
29 | def test_file_configuration_loading(self, ghidra_initialized, tmp_path):
30 | """Configuration can be loaded from properties file"""
31 | from reva.plugin import ConfigManager
32 |
33 | # Create config file
34 | config_file = tmp_path / "test.properties"
35 | config_file.write_text(
36 | "reva.server.options.server.port=7777\n"
37 | "reva.server.options.server.host=localhost\n"
38 | )
39 |
40 | # Load config from file
41 | config = ConfigManager(str(config_file))
42 |
43 | # Should use configured port
44 | port = config.getPort()
45 | assert port == 7777
46 |
47 | def test_config_file_with_multiple_options(self, ghidra_initialized, tmp_path):
48 | """Configuration file supports multiple options"""
49 | from reva.plugin import ConfigManager
50 |
51 | config_file = tmp_path / "full.properties"
52 | config_file.write_text("""
53 | # Server options
54 | reva.server.options.server.port=8888
55 | reva.server.options.server.host=127.0.0.1
56 |
57 | # Debug options
58 | reva.server.options.debug.mode=true
59 | """)
60 |
61 | config = ConfigManager(str(config_file))
62 |
63 | # Verify port loaded correctly
64 | assert config.getPort() == 8888
65 |
66 |
67 | class TestConfigurationEdgeCases:
68 | """Test configuration edge cases"""
69 |
70 | def test_missing_config_file(self, ghidra_initialized, tmp_path):
71 | """Missing config file falls back to defaults gracefully"""
72 | from reva.plugin import ConfigManager
73 |
74 | nonexistent = tmp_path / "missing.properties"
75 |
76 | # Should not raise - gracefully falls back to defaults
77 | config = ConfigManager(str(nonexistent))
78 |
79 | # Should use default port
80 | port = config.getPort()
81 | assert port == 8080
82 |
83 | def test_empty_config_file(self, ghidra_initialized, tmp_path):
84 | """Empty config file uses defaults"""
85 | from reva.plugin import ConfigManager
86 |
87 | config_file = tmp_path / "empty.properties"
88 | config_file.write_text("")
89 |
90 | config = ConfigManager(str(config_file))
91 |
92 | # Should use default port
93 | port = config.getPort()
94 | assert port == 8080
95 |
96 | def test_config_file_with_comments(self, ghidra_initialized, tmp_path):
97 | """Config file handles comments correctly"""
98 | from reva.plugin import ConfigManager
99 |
100 | config_file = tmp_path / "commented.properties"
101 | config_file.write_text("""
102 | # This is a comment
103 | reva.server.options.server.port=6666
104 | # Another comment
105 | """)
106 |
107 | config = ConfigManager(str(config_file))
108 |
109 | # Should parse port despite comments
110 | assert config.getPort() == 6666
111 |
--------------------------------------------------------------------------------
/data/languages/skel.cspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/tests/test_launcher.py:
--------------------------------------------------------------------------------
1 | """
2 | Test RevaHeadlessLauncher lifecycle management.
3 |
4 | Verifies that:
5 | - Launcher can start and stop
6 | - Server becomes ready within timeout
7 | - Configuration options are respected
8 | - Multiple start/stop cycles work
9 | """
10 |
11 | import pytest
12 | from pathlib import Path
13 |
14 |
15 | class TestLauncherLifecycle:
16 | """Test ReVa headless launcher lifecycle"""
17 |
18 | def test_launcher_starts_and_stops(self, ghidra_initialized):
19 | """Launcher can start and stop cleanly"""
20 | from reva.headless import RevaHeadlessLauncher
21 |
22 | launcher = RevaHeadlessLauncher()
23 |
24 | # Should not be running initially
25 | assert not launcher.isRunning()
26 | assert not launcher.isServerReady()
27 |
28 | # Start server
29 | launcher.start()
30 |
31 | # Wait for server to be ready
32 | ready = launcher.waitForServer(30000)
33 | assert ready, "Server failed to become ready within 30 seconds"
34 |
35 | # Verify status
36 | assert launcher.isRunning()
37 | assert launcher.isServerReady()
38 |
39 | # Should have valid port
40 | port = launcher.getPort()
41 | assert 1024 < port < 65535
42 |
43 | # Stop server
44 | launcher.stop()
45 |
46 | # Should not be running after stop
47 | assert not launcher.isRunning()
48 |
49 | def test_launcher_timeout_on_wait(self, ghidra_initialized):
50 | """waitForServer returns False if called before start"""
51 | from reva.headless import RevaHeadlessLauncher
52 |
53 | launcher = RevaHeadlessLauncher()
54 |
55 | # Should timeout immediately since server not started
56 | ready = launcher.waitForServer(1000)
57 | assert not ready
58 |
59 | def test_server_fixture_provides_ready_server(self, server):
60 | """Server fixture provides a running and ready server"""
61 | assert server.isRunning()
62 | assert server.isServerReady()
63 |
64 | port = server.getPort()
65 | assert 1024 < port < 65535
66 |
67 |
68 | class TestLauncherConfiguration:
69 | """Test launcher configuration options"""
70 |
71 | def test_launcher_with_default_config(self, ghidra_initialized):
72 | """Launcher works with default configuration"""
73 | from reva.headless import RevaHeadlessLauncher
74 |
75 | launcher = RevaHeadlessLauncher()
76 | launcher.start()
77 |
78 | assert launcher.waitForServer(30000)
79 |
80 | # Default port should be 8080
81 | port = launcher.getPort()
82 | assert port == 8080
83 |
84 | launcher.stop()
85 |
86 | def test_launcher_with_custom_config(self, ghidra_initialized, tmp_path):
87 | """Launcher respects configuration file"""
88 | from reva.headless import RevaHeadlessLauncher
89 |
90 | # Create config file with custom port
91 | config_file = tmp_path / "test.properties"
92 | config_file.write_text(
93 | "reva.server.options.server.port=9999\n"
94 | "reva.server.options.server.host=127.0.0.1\n"
95 | )
96 |
97 | # Create launcher with config file
98 | launcher = RevaHeadlessLauncher(str(config_file))
99 | launcher.start()
100 |
101 | assert launcher.waitForServer(30000)
102 |
103 | # Should use configured port
104 | port = launcher.getPort()
105 | assert port == 9999
106 |
107 | launcher.stop()
108 |
109 | def test_launcher_with_missing_config_file(self, ghidra_initialized, tmp_path):
110 | """Launcher handles missing config file gracefully with defaults"""
111 | from reva.headless import RevaHeadlessLauncher
112 |
113 | # Create launcher with non-existent config - should use defaults
114 | nonexistent = tmp_path / "does_not_exist.properties"
115 | launcher = RevaHeadlessLauncher(str(nonexistent))
116 |
117 | # Should start successfully with default config
118 | launcher.start()
119 | assert launcher.waitForServer(30000)
120 |
121 | # Should use default port
122 | port = launcher.getPort()
123 | assert port == 8080
124 |
125 | launcher.stop()
126 |
--------------------------------------------------------------------------------
/.github/workflows/publish-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distribution to PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 | branches:
8 | - 'develop'
9 | workflow_dispatch:
10 | inputs:
11 | publish_to:
12 | description: 'Publish to PyPI, TestPyPI, or both'
13 | required: true
14 | type: choice
15 | options:
16 | - 'pypi'
17 | - 'testpypi'
18 | - 'both'
19 | default: 'pypi'
20 |
21 | jobs:
22 | build:
23 | name: Build Python distribution 📦
24 | runs-on: ubuntu-latest
25 | permissions:
26 | contents: read
27 | steps:
28 | - uses: actions/checkout@v5
29 | with:
30 | # Fetch all history for setuptools_scm to determine version from git tags
31 | fetch-depth: 0
32 |
33 | - name: Set up Python 🐍
34 | uses: actions/setup-python@v6
35 | with:
36 | python-version: "3.12"
37 |
38 | - name: Install build dependencies
39 | run: |
40 | python -m pip install --upgrade pip
41 | pip install build setuptools_scm
42 |
43 | - name: Verify version extraction
44 | run: |
45 | python -m setuptools_scm
46 | echo "Package version will be: $(python -m setuptools_scm)"
47 |
48 | - name: Build distribution packages
49 | run: python -m build
50 |
51 | - name: Store distribution packages
52 | uses: actions/upload-artifact@v5
53 | with:
54 | name: python-package-distributions
55 | path: dist/
56 | if-no-files-found: error
57 |
58 | publish-to-testpypi:
59 | name: Publish to TestPyPI 🧪
60 | # Only run on develop branch pushes or manual dispatch with testpypi/both
61 | if: |
62 | github.ref == 'refs/heads/develop' ||
63 | (github.event_name == 'workflow_dispatch' &&
64 | (github.event.inputs.publish_to == 'testpypi' || github.event.inputs.publish_to == 'both'))
65 | needs:
66 | - build
67 | runs-on: ubuntu-latest
68 | environment:
69 | name: testpypi
70 | url: https://test.pypi.org/p/reverse-engineering-assistant
71 | permissions:
72 | id-token: write # IMPORTANT: mandatory for trusted publishing
73 | steps:
74 | - name: Download distributions
75 | uses: actions/download-artifact@v6
76 | with:
77 | name: python-package-distributions
78 | path: dist/
79 |
80 | - name: Publish to TestPyPI
81 | uses: pypa/gh-action-pypi-publish@release/v1
82 | with:
83 | repository-url: https://test.pypi.org/legacy/
84 | skip-existing: true
85 | verbose: true
86 |
87 | publish-to-pypi:
88 | name: Publish to PyPI 🚀
89 | # Only run on tag pushes or manual dispatch with pypi/both
90 | if: |
91 | startsWith(github.ref, 'refs/tags/v') ||
92 | (github.event_name == 'workflow_dispatch' &&
93 | (github.event.inputs.publish_to == 'pypi' || github.event.inputs.publish_to == 'both'))
94 | needs:
95 | - build
96 | runs-on: ubuntu-latest
97 | environment:
98 | name: pypi
99 | url: https://pypi.org/p/reverse-engineering-assistant
100 | permissions:
101 | contents: write # IMPORTANT: mandatory for GitHub Releases
102 | id-token: write # IMPORTANT: mandatory for trusted publishing and sigstore
103 | steps:
104 | - name: Download distributions
105 | uses: actions/download-artifact@v6
106 | with:
107 | name: python-package-distributions
108 | path: dist/
109 |
110 | - name: Sign distributions with Sigstore
111 | uses: sigstore/gh-action-sigstore-python@v3.1.0
112 | with:
113 | inputs: >-
114 | ./dist/*.tar.gz
115 | ./dist/*.whl
116 | upload-signing-artifacts: true
117 |
118 | - name: Upload signatures to GitHub Release
119 | if: startsWith(github.ref, 'refs/tags/v')
120 | env:
121 | GITHUB_TOKEN: ${{ github.token }}
122 | run: |
123 | # Upload Python package signatures to existing release created by publish-ghidra.yml
124 | gh release upload '${{ github.ref_name }}' ./dist/*.sigstore* --clobber \
125 | --repo '${{ github.repository }}'
126 |
127 | - name: Publish to PyPI
128 | uses: pypa/gh-action-pypi-publish@release/v1
129 | with:
130 | verbose: true
131 |
--------------------------------------------------------------------------------
/src/main/java/reva/util/DebugLogger.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.util;
17 |
18 | import ghidra.util.Msg;
19 | import reva.plugin.ConfigManager;
20 | import reva.util.RevaInternalServiceRegistry;
21 |
22 | /**
23 | * Debug logger utility that respects the debug configuration setting.
24 | * Provides specialized logging for connection debugging and performance monitoring.
25 | */
26 | public class DebugLogger {
27 |
28 | private static ConfigManager getConfigManager() {
29 | return RevaInternalServiceRegistry.getService(ConfigManager.class);
30 | }
31 |
32 | /**
33 | * Log a debug message if debug mode is enabled
34 | * @param source The source object for the log message
35 | * @param message The message to log
36 | */
37 | public static void debug(Object source, String message) {
38 | ConfigManager config = getConfigManager();
39 | if (config != null && config.isDebugMode()) {
40 | Msg.info(source, "[DEBUG] " + message);
41 | }
42 | }
43 |
44 | /**
45 | * Log a debug message with an exception if debug mode is enabled
46 | * @param source The source object for the log message
47 | * @param message The message to log
48 | * @param throwable The exception to include
49 | */
50 | public static void debug(Object source, String message, Throwable throwable) {
51 | ConfigManager config = getConfigManager();
52 | if (config != null && config.isDebugMode()) {
53 | Msg.info(source, "[DEBUG] " + message, throwable);
54 | }
55 | }
56 |
57 | /**
58 | * Log a connection-related debug message if debug mode is enabled
59 | * @param source The source object for the log message
60 | * @param message The message to log
61 | */
62 | public static void debugConnection(Object source, String message) {
63 | ConfigManager config = getConfigManager();
64 | if (config != null && config.isDebugMode()) {
65 | Msg.info(source, "[DEBUG-CONNECTION] " + message);
66 | }
67 | }
68 |
69 | /**
70 | * Log a performance-related debug message if debug mode is enabled
71 | * @param source The source object for the log message
72 | * @param operation The operation being timed
73 | * @param durationMs The duration in milliseconds
74 | */
75 | public static void debugPerformance(Object source, String operation, long durationMs) {
76 | ConfigManager config = getConfigManager();
77 | if (config != null && config.isDebugMode()) {
78 | Msg.info(source, "[DEBUG-PERF] " + operation + " took " + durationMs + "ms");
79 | }
80 | }
81 |
82 | /**
83 | * Log a tool execution debug message if debug mode is enabled
84 | * @param source The source object for the log message
85 | * @param toolName The name of the tool being executed
86 | * @param status The status (START, END, ERROR, etc.)
87 | * @param details Additional details
88 | */
89 | public static void debugToolExecution(Object source, String toolName, String status, String details) {
90 | ConfigManager config = getConfigManager();
91 | if (config != null && config.isDebugMode()) {
92 | String message = "[DEBUG-TOOL] " + toolName + " - " + status;
93 | if (details != null && !details.isEmpty()) {
94 | message += ": " + details;
95 | }
96 | Msg.info(source, message);
97 | }
98 | }
99 |
100 | /**
101 | * Check if debug mode is currently enabled
102 | * @return true if debug mode is enabled, false otherwise
103 | */
104 | public static boolean isDebugEnabled() {
105 | ConfigManager config = getConfigManager();
106 | return config != null && config.isDebugMode();
107 | }
108 | }
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/config/InMemoryBackend.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin.config;
17 |
18 | import java.util.HashMap;
19 | import java.util.Map;
20 | import java.util.Set;
21 | import java.util.concurrent.ConcurrentHashMap;
22 |
23 | import ghidra.util.Msg;
24 |
25 | /**
26 | * In-memory configuration backend.
27 | * Stores configuration in memory with no persistence.
28 | * Used for headless mode with default settings or testing.
29 | */
30 | public class InMemoryBackend implements ConfigurationBackend {
31 |
32 | private final Map storage = new ConcurrentHashMap<>();
33 | private final Set listeners = ConcurrentHashMap.newKeySet();
34 |
35 | @Override
36 | public int getInt(String category, String name, int defaultValue) {
37 | String key = makeKey(category, name);
38 | Object value = storage.get(key);
39 | if (value instanceof Integer) {
40 | return (Integer) value;
41 | }
42 | return defaultValue;
43 | }
44 |
45 | @Override
46 | public void setInt(String category, String name, int value) {
47 | String key = makeKey(category, name);
48 | Object oldValue = storage.put(key, value);
49 | notifyListeners(category, name, oldValue, value);
50 | }
51 |
52 | @Override
53 | public String getString(String category, String name, String defaultValue) {
54 | String key = makeKey(category, name);
55 | Object value = storage.get(key);
56 | if (value instanceof String) {
57 | return (String) value;
58 | }
59 | return defaultValue;
60 | }
61 |
62 | @Override
63 | public void setString(String category, String name, String value) {
64 | String key = makeKey(category, name);
65 | Object oldValue = storage.put(key, value);
66 | notifyListeners(category, name, oldValue, value);
67 | }
68 |
69 | @Override
70 | public boolean getBoolean(String category, String name, boolean defaultValue) {
71 | String key = makeKey(category, name);
72 | Object value = storage.get(key);
73 | if (value instanceof Boolean) {
74 | return (Boolean) value;
75 | }
76 | return defaultValue;
77 | }
78 |
79 | @Override
80 | public void setBoolean(String category, String name, boolean value) {
81 | String key = makeKey(category, name);
82 | Object oldValue = storage.put(key, value);
83 | notifyListeners(category, name, oldValue, value);
84 | }
85 |
86 | @Override
87 | public boolean supportsChangeNotifications() {
88 | return true;
89 | }
90 |
91 | @Override
92 | public void addChangeListener(ConfigurationBackendListener listener) {
93 | listeners.add(listener);
94 | }
95 |
96 | @Override
97 | public void removeChangeListener(ConfigurationBackendListener listener) {
98 | listeners.remove(listener);
99 | }
100 |
101 | @Override
102 | public void dispose() {
103 | storage.clear();
104 | listeners.clear();
105 | }
106 |
107 | /**
108 | * Create a storage key from category and name
109 | */
110 | private String makeKey(String category, String name) {
111 | return category + ":" + name;
112 | }
113 |
114 | /**
115 | * Notify all listeners of a configuration change
116 | */
117 | private void notifyListeners(String category, String name, Object oldValue, Object newValue) {
118 | for (ConfigurationBackendListener listener : listeners) {
119 | try {
120 | listener.onConfigurationChanged(category, name, oldValue, newValue);
121 | } catch (Exception e) {
122 | Msg.error(this, "Error notifying configuration listener", e);
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/.github/workflows/test-ghidra.yml:
--------------------------------------------------------------------------------
1 | name: Test Ghidra 🐉 Extension
2 |
3 | on:
4 | push:
5 | branches: [ main, develop ]
6 | pull_request:
7 | branches: [ main, develop ]
8 |
9 | permissions:
10 | contents: write
11 | actions: read
12 | checks: write
13 | security-events: write
14 |
15 | jobs:
16 | test:
17 | strategy:
18 | matrix:
19 | os: [ubuntu-latest]
20 | ghidra-version: [
21 | "12.0",
22 | "latest",
23 | ]
24 | name: Test on Ghidra ${{ matrix.ghidra-version }}
25 | runs-on: ${{ matrix.os }}
26 | steps:
27 | - uses: actions/checkout@v5
28 |
29 | - name: Set up Java 🍵
30 | uses: actions/setup-java@v5
31 | with:
32 | java-version: "21"
33 | distribution: "microsoft"
34 |
35 | - name: Install Ghidra 🐉
36 | uses: antoniovazquezblanco/setup-ghidra@v2.0.15
37 | with:
38 | version: ${{ matrix.ghidra-version }}
39 |
40 | - name: Setup Xvfb for headless testing
41 | run: |
42 | sudo apt-get update && sudo apt-get install -y xvfb
43 | Xvfb :99 -nolisten tcp &
44 | echo "DISPLAY=:99" >> $GITHUB_ENV
45 |
46 |
47 | - name: Setup Gradle 🔧
48 | uses: gradle/actions/setup-gradle@v5
49 | with:
50 | gradle-version: "8.14"
51 |
52 | - name: Build Extension 🔨
53 | run: gradle buildExtension
54 |
55 | - name: Run Unit Tests 🧪
56 | run: gradle test --info
57 | env:
58 | _JAVA_OPTIONS: "-Djava.awt.headless=true"
59 |
60 | - name: Run Integration Tests 🖥️
61 | run: gradle integrationTest --info
62 | env:
63 | DISPLAY: :99
64 |
65 | - name: Upload test results
66 | uses: actions/upload-artifact@v5
67 | if: always()
68 | with:
69 | name: test-results-${{ matrix.ghidra-version }}
70 | path: |
71 | build/reports/tests/
72 | build/test-results/
73 | build/reports/integrationTest/
74 | build/test-results/integrationTest/
75 | if-no-files-found: ignore
76 |
77 | - name: Test Summary
78 | uses: test-summary/action@v2
79 | with:
80 | show: "fail, skip"
81 | paths: |
82 | build/test-results/**/TEST-*.xml
83 | if: always()
84 |
85 | lint:
86 | name: Lint and Check 🔍
87 | runs-on: ubuntu-latest
88 | steps:
89 | - uses: actions/checkout@v5
90 |
91 | - name: Set up Java 🍵
92 | uses: actions/setup-java@v5
93 | with:
94 | java-version: "21"
95 | distribution: "microsoft"
96 |
97 | - name: Install Ghidra 🐉
98 | uses: antoniovazquezblanco/setup-ghidra@v2.0.15
99 | with:
100 | version: "latest"
101 |
102 | - name: Setup Xvfb for headless testing
103 | run: |
104 | sudo apt-get update && sudo apt-get install -y xvfb
105 | Xvfb :99 -nolisten tcp &
106 | echo "DISPLAY=:99" >> $GITHUB_ENV
107 |
108 | - name: Setup Gradle 🔧
109 | uses: gradle/actions/setup-gradle@v5
110 | with:
111 | gradle-version: "8.14"
112 | dependency-graph: generate-and-submit
113 | dependency-graph-continue-on-failure: true
114 |
115 | - name: Check code compilation
116 | run: gradle compileJava
117 |
118 | - name: Check test compilation
119 | run: gradle compileTestJava
120 |
121 | codeql:
122 | name: CodeQL Security Analysis 🔒
123 | runs-on: ubuntu-latest
124 | permissions:
125 | actions: read
126 | contents: read
127 | security-events: write
128 |
129 | steps:
130 | - uses: actions/checkout@v5
131 |
132 | - name: Set up Java 🍵
133 | uses: actions/setup-java@v5
134 | with:
135 | java-version: "21"
136 | distribution: "microsoft"
137 |
138 | - name: Initialize CodeQL
139 | uses: github/codeql-action/init@v4
140 | with:
141 | languages: java
142 | # Use security-extended queries for more thorough analysis
143 | queries: security-extended,security-and-quality
144 |
145 | - name: Install Ghidra 🐉
146 | uses: antoniovazquezblanco/setup-ghidra@v2.0.15
147 | with:
148 | version: "latest"
149 |
150 |
151 | - name: Setup Gradle for CodeQL 🔧
152 | uses: gradle/actions/setup-gradle@v5
153 | with:
154 | gradle-version: "8.14"
155 |
156 | - name: Build Extension for CodeQL Analysis 🔨
157 | run: gradle buildExtension
158 |
159 | - name: Perform CodeQL Analysis
160 | uses: github/codeql-action/analyze@v4
161 | with:
162 | category: "/language:java"
163 |
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/config/ToolOptionsBackend.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin.config;
17 |
18 | import java.util.Set;
19 | import java.util.concurrent.ConcurrentHashMap;
20 |
21 | import ghidra.framework.options.OptionsChangeListener;
22 | import ghidra.framework.options.ToolOptions;
23 | import ghidra.framework.plugintool.PluginTool;
24 | import ghidra.util.Msg;
25 | import ghidra.util.bean.opteditor.OptionsVetoException;
26 |
27 | /**
28 | * Configuration backend that uses Ghidra's ToolOptions.
29 | * This is used in GUI mode where configuration is persisted to Ghidra's tool options.
30 | */
31 | public class ToolOptionsBackend implements ConfigurationBackend, OptionsChangeListener {
32 |
33 | private final PluginTool tool;
34 | private final ToolOptions toolOptions;
35 | private final Set listeners = ConcurrentHashMap.newKeySet();
36 |
37 | /**
38 | * Constructor
39 | * @param tool The plugin tool
40 | * @param category The options category (e.g., "ReVa Server Options")
41 | */
42 | public ToolOptionsBackend(PluginTool tool, String category) {
43 | this.tool = tool;
44 | this.toolOptions = tool.getOptions(category);
45 |
46 | // Register as listener for Ghidra's option changes
47 | toolOptions.addOptionsChangeListener(this);
48 | }
49 |
50 | @Override
51 | public int getInt(String category, String name, int defaultValue) {
52 | return toolOptions.getInt(name, defaultValue);
53 | }
54 |
55 | @Override
56 | public void setInt(String category, String name, int value) {
57 | toolOptions.setInt(name, value);
58 | // optionsChanged() will be called automatically by Ghidra
59 | }
60 |
61 | @Override
62 | public String getString(String category, String name, String defaultValue) {
63 | return toolOptions.getString(name, defaultValue);
64 | }
65 |
66 | @Override
67 | public void setString(String category, String name, String value) {
68 | toolOptions.setString(name, value);
69 | // optionsChanged() will be called automatically by Ghidra
70 | }
71 |
72 | @Override
73 | public boolean getBoolean(String category, String name, boolean defaultValue) {
74 | return toolOptions.getBoolean(name, defaultValue);
75 | }
76 |
77 | @Override
78 | public void setBoolean(String category, String name, boolean value) {
79 | toolOptions.setBoolean(name, value);
80 | // optionsChanged() will be called automatically by Ghidra
81 | }
82 |
83 | @Override
84 | public boolean supportsChangeNotifications() {
85 | return true;
86 | }
87 |
88 | @Override
89 | public void addChangeListener(ConfigurationBackendListener listener) {
90 | listeners.add(listener);
91 | }
92 |
93 | @Override
94 | public void removeChangeListener(ConfigurationBackendListener listener) {
95 | listeners.remove(listener);
96 | }
97 |
98 | @Override
99 | public void dispose() {
100 | if (toolOptions != null) {
101 | toolOptions.removeOptionsChangeListener(this);
102 | }
103 | listeners.clear();
104 | }
105 |
106 | /**
107 | * Ghidra's options change callback
108 | */
109 | @Override
110 | public void optionsChanged(ToolOptions options, String optionName, Object oldValue, Object newValue)
111 | throws OptionsVetoException {
112 |
113 | Msg.debug(this, "ToolOptions changed: " + optionName + " from " + oldValue + " to " + newValue);
114 |
115 | // Notify our listeners
116 | // Note: We pass empty string as category since ToolOptions doesn't provide it
117 | for (ConfigurationBackendListener listener : listeners) {
118 | try {
119 | listener.onConfigurationChanged("", optionName, oldValue, newValue);
120 | } catch (Exception e) {
121 | Msg.error(this, "Error notifying configuration listener", e);
122 | }
123 | }
124 | }
125 |
126 | /**
127 | * Get the underlying ToolOptions (for registering options)
128 | * @return The ToolOptions instance
129 | */
130 | public ToolOptions getToolOptions() {
131 | return toolOptions;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/tools/comments/CommentToolProviderIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools.comments;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import java.util.HashMap;
21 | import java.util.Map;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 |
26 | import com.fasterxml.jackson.databind.JsonNode;
27 |
28 | import ghidra.program.model.address.Address;
29 | import ghidra.program.model.listing.CommentType;
30 | import ghidra.program.model.listing.Listing;
31 | import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
32 | import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
33 | import io.modelcontextprotocol.spec.McpSchema.TextContent;
34 |
35 | import reva.RevaIntegrationTestBase;
36 |
37 | /**
38 | * Integration tests for CommentToolProvider using MCP client.
39 | * Tests the full end-to-end flow from MCP client through the server to Ghidra.
40 | */
41 | public class CommentToolProviderIntegrationTest extends RevaIntegrationTestBase {
42 |
43 | private String programPath;
44 |
45 | @Before
46 | public void setUpTestData() throws Exception {
47 | programPath = program.getDomainFile().getPathname();
48 |
49 | // Open the program in the tool's ProgramManager so it can be found by RevaProgramManager
50 | env.open(program);
51 |
52 | // Also open it directly in the tool's ProgramManager service to ensure it's available
53 | ghidra.app.services.ProgramManager programManager = tool.getService(ghidra.app.services.ProgramManager.class);
54 | if (programManager != null) {
55 | programManager.openProgram(program);
56 | }
57 |
58 | // Register the program with the server manager so it can be found by the tools
59 | if (serverManager != null) {
60 | serverManager.programOpened(program, tool);
61 | }
62 | }
63 |
64 | @Test
65 | public void testSetAndGetComment() throws Exception {
66 | withMcpClient(createMcpTransport(), client -> {
67 | try {
68 | client.initialize();
69 |
70 | // Use the minimum address in the program which should be valid
71 | Address testAddress = program.getMinAddress();
72 | String addressStr = testAddress.toString();
73 |
74 | // Set a comment
75 | Map setArgs = new HashMap<>();
76 | setArgs.put("programPath", programPath);
77 | setArgs.put("addressOrSymbol", addressStr);
78 | setArgs.put("commentType", "eol");
79 | setArgs.put("comment", "Test comment");
80 |
81 | CallToolRequest setRequest = new CallToolRequest("set-comment", setArgs);
82 | CallToolResult setResult = client.callTool(setRequest);
83 | assertFalse("Set comment should succeed", setResult.isError());
84 |
85 | // Verify the comment was set in the program
86 | Listing listing = program.getListing();
87 | String actualComment = listing.getComment(CommentType.EOL, testAddress);
88 | assertEquals("Comment should be set correctly", "Test comment", actualComment);
89 |
90 | // Get the comment using the tool
91 | Map getArgs = new HashMap<>();
92 | getArgs.put("programPath", programPath);
93 | getArgs.put("addressOrSymbol", addressStr);
94 |
95 | CallToolRequest getRequest = new CallToolRequest("get-comments", getArgs);
96 | CallToolResult getResult = client.callTool(getRequest);
97 | assertFalse("Get comments should succeed", getResult.isError());
98 |
99 | // Parse the result
100 | String jsonResponse = ((TextContent) getResult.content().get(0)).text();
101 | JsonNode responseNode = objectMapper.readTree(jsonResponse);
102 | JsonNode commentsNode = responseNode.get("comments");
103 |
104 | assertEquals("Should have one comment", 1, commentsNode.size());
105 | assertEquals("Comment text should match", "Test comment", commentsNode.get(0).get("comment").asText());
106 | assertEquals("Comment type should match", "eol", commentsNode.get(0).get("commentType").asText());
107 | } catch (Exception e) {
108 | fail("Test failed with exception: " + e.getMessage());
109 | }
110 | });
111 | }
112 | }
--------------------------------------------------------------------------------
/src/test.slow/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # Integration Test Guidelines
2 |
3 | ## Base Class and Infrastructure
4 | All integration tests extend `RevaIntegrationTestBase` which provides:
5 | - Shared Ghidra environment (TestEnv, PluginTool, MCP server) across all tests in a class
6 | - Fresh `Program` instance for each test with pre-configured memory block at 0x01000000
7 | - MCP client utilities and helper methods for calling tools
8 | - Automatic program registration/unregistration with the MCP server
9 |
10 | ## Test Environment Configuration
11 | **Build Configuration** (build.gradle):
12 | - Tests use `forkEvery 1` - each test runs in its own JVM to prevent state conflicts
13 | - Tests require `java.awt.headless=false` - GUI environment required for Ghidra
14 | - Run with: `gradle integrationTest --info`
15 |
16 | **Shared Environment Pattern**:
17 | - `@BeforeClass` (static): Lazy initialization of shared TestEnv, PluginTool, and MCP server
18 | - `@Before` (instance): Creates fresh Program, registers with server, opens in tool
19 | - `@After` (instance): Unregisters and releases program, keeps shared environment running
20 | - `@AfterClass` (static): Shuts down shared environment after all tests complete
21 |
22 | ## Writing Integration Tests
23 |
24 | ### Basic Test Structure
25 | ```java
26 | public class MyToolIntegrationTest extends RevaIntegrationTestBase {
27 | private String programPath;
28 |
29 | @Before
30 | public void setUpTestData() throws Exception {
31 | programPath = program.getDomainFile().getPathname();
32 |
33 | // Set up test data in program using transactions
34 | int txId = program.startTransaction("Setup test data");
35 | try {
36 | // Create functions, data, symbols, etc.
37 | } finally {
38 | program.endTransaction(txId, true);
39 | }
40 |
41 | // Open program in tool (usually needed for tools to find it)
42 | env.open(program);
43 | }
44 |
45 | @Test
46 | public void testMyTool() throws Exception {
47 | withMcpClient(createMcpTransport(), client -> {
48 | client.initialize();
49 |
50 | // Call the MCP tool
51 | CallToolResult result = client.callTool(
52 | new CallToolRequest("my-tool", Map.of("programPath", programPath))
53 | );
54 |
55 | // Validate MCP response
56 | assertMcpResultNotError(result, "Tool should not error");
57 | TextContent content = (TextContent) result.content().get(0);
58 | JsonNode json = parseJsonContent(content.text());
59 | assertEquals(expectedValue, json.get("field").asText());
60 |
61 | // **CRITICAL**: Validate actual program state changes
62 | FunctionManager fm = program.getFunctionManager();
63 | Function func = fm.getFunctionAt(testAddr);
64 | assertEquals("Expected name", func.getName());
65 | });
66 | }
67 | }
68 | ```
69 |
70 | ### Critical Validation Pattern
71 | **DO NOT** only check MCP tool responses. **ALWAYS** validate actual Ghidra program state:
72 | - Use `Function.getParameters()` and `Function.getAllVariables()` to verify variable changes
73 | - Use `DataType.isEquivalent()` to compare datatypes before/after modifications
74 | - Use `FunctionManager`, `Listing`, `SymbolTable`, etc. to verify state changes
75 | - Example: After renaming a function, check `program.getFunctionManager().getFunctionAt(addr).getName()`
76 |
77 | ### Helper Methods Available
78 | - `createMcpTransport()` - Creates HTTP transport to MCP server
79 | - `withMcpClient(transport, client -> {...})` - Execute operations with auto-closing client
80 | - `parseJsonContent(String)` - Parse JSON from MCP TextContent
81 | - `assertMcpResultNotError(result, message)` - Assert MCP result is not an error
82 | - `callMcpTool(toolName, args)` - Simplified tool call that returns content string
83 | - `getAvailableTools()` - List all registered tools
84 |
85 | ### Common Patterns
86 | - **Addresses**: Use addresses within the default memory block (0x01000000 to 0x01001000)
87 | - **Transactions**: Always wrap program modifications in `startTransaction()` / `endTransaction()`
88 | - **Program registration**: Base class handles registration, but may need `env.open(program)` for ProgramManager
89 | - **Error testing**: Use `assertTrue(result.isError())` to verify expected errors
90 | - **JSON parsing**: Use `objectMapper.readTree()` or `parseJsonContent()` helper
91 |
92 | ## Performance Benefits
93 | - **Faster execution**: Shared environment eliminates 5-10 second Ghidra startup per test
94 | - **Stable connections**: MCP server runs once per test class, not per test
95 | - **Resource efficiency**: Single Ghidra instance serves all tests in a class
96 | - **Test isolation**: Fresh program per test prevents state leakage
97 |
98 | ## Common Issues
99 | - **Fork requirement**: Tests run in separate JVMs (forkEvery=1) - don't rely on static state between tests
100 | - **Headless mode**: Tests fail if run with `java.awt.headless=true`
101 | - **Transaction leaks**: Always close transactions in finally blocks
102 | - **Program not found**: Ensure `env.open(program)` is called if tools can't find the program
103 | - **JSON parsing**: Use helper methods, don't parse manually
--------------------------------------------------------------------------------
/tests/test_mcp_tools.py:
--------------------------------------------------------------------------------
1 | """
2 | Test ReVa MCP tool functionality.
3 |
4 | Verifies that:
5 | - Tools can be called and return results
6 | - list-open-programs works
7 | - get-strings works
8 | - get-functions works
9 | - Other key tools are accessible
10 | """
11 |
12 | import pytest
13 | from tests.helpers import get_response_result
14 |
15 |
16 | class TestProgramTools:
17 | """Test program-related MCP tools"""
18 |
19 | def test_list_programs(self, mcp_client):
20 | """list-open-programs tool returns program list (may be empty)"""
21 | response = mcp_client.call_tool("list-open-programs")
22 |
23 | # Should get a response (even if no programs are open)
24 | assert response is not None
25 |
26 | # If it's an error response, it should be about no programs being open
27 | if response.get("isError", False):
28 | error_msg = str(response.get("content", ""))
29 | assert "No programs" in error_msg or "no program" in error_msg.lower()
30 | else:
31 | # If success, should have content that's a list
32 | result = get_response_result(response)
33 | assert "content" in result
34 | content = result["content"]
35 | assert isinstance(content, list)
36 |
37 | def test_list_programs_includes_format(self, mcp_client, test_program):
38 | """list-open-programs result has expected structure when programs are open"""
39 | # This test uses test_program fixture to ensure at least one program is available
40 | # Note: test_program may not be registered with MCP server's project manager
41 | response = mcp_client.call_tool("list-open-programs")
42 |
43 | # Handle case where no programs are open in the MCP server's project
44 | if response.get("isError", False):
45 | # Expected - test_program isn't registered with MCP server
46 | assert response is not None
47 | return
48 |
49 | result = get_response_result(response)
50 |
51 | # Should have content
52 | assert "content" in result
53 | content = result["content"]
54 |
55 | # Content should be list of objects with type and text
56 | # May be empty if program isn't registered with MCP server
57 | if len(content) > 0:
58 | for item in content:
59 | assert "type" in item
60 | assert "text" in item
61 |
62 |
63 | class TestStringTools:
64 | """Test string analysis tools"""
65 |
66 | def test_list_strings_requires_program(self, mcp_client):
67 | """get-strings requires programPath argument"""
68 | response = mcp_client.call_tool("get-strings", {
69 | "programPath": "/NonexistentProgram",
70 | "maxCount": 5
71 | })
72 |
73 | # Should get a response (even if error due to missing program)
74 | assert response is not None
75 |
76 | # Will likely error since program doesn't exist, but that's okay
77 | # We're just testing the tool is registered and callable
78 |
79 | def test_list_strings_with_valid_program_path(self, mcp_client):
80 | """get-strings accepts valid programPath format"""
81 | # We don't have a real project, but we can verify the tool accepts
82 | # properly formatted requests
83 | response = mcp_client.call_tool("get-strings", {
84 | "programPath": "/TestProgram.exe",
85 | "maxCount": 10
86 | })
87 |
88 | # Should get response (even if error about program not existing)
89 | assert response is not None
90 |
91 |
92 | class TestFunctionTools:
93 | """Test function-related MCP tools"""
94 |
95 | def test_list_functions_callable(self, mcp_client):
96 | """get-functions tool is registered and callable"""
97 | response = mcp_client.call_tool("get-functions", {
98 | "programPath": "/TestProgram"
99 | })
100 |
101 | # Should get a response
102 | assert response is not None
103 |
104 | def test_get_decompilation_callable(self, mcp_client):
105 | """get-decompilation tool is registered and callable"""
106 | response = mcp_client.call_tool("get-decompilation", {
107 | "programPath": "/TestProgram",
108 | "address": "0x00401000"
109 | })
110 |
111 | # Should get a response (even if error)
112 | assert response is not None
113 |
114 |
115 | class TestToolRegistration:
116 | """Test that key tools are registered"""
117 |
118 | @pytest.mark.parametrize("tool_name", [
119 | "list-open-programs",
120 | "get-functions",
121 | "get-strings",
122 | "get-decompilation",
123 | "analyze-program",
124 | "find-cross-references"
125 | ])
126 | def test_tool_is_registered(self, mcp_client, tool_name):
127 | """All expected tools are registered and callable"""
128 | # Call with minimal args - we just want to verify tool exists
129 | response = mcp_client.call_tool(tool_name, {})
130 |
131 | # Should get some response (even if error due to missing required args)
132 | # The key is that we get a response, not a connection error
133 | assert response is not None
134 |
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/RevaPlugin.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | import java.util.List;
19 |
20 | import ghidra.app.plugin.PluginCategoryNames;
21 | import ghidra.app.plugin.ProgramPlugin;
22 | import ghidra.framework.plugintool.PluginInfo;
23 | import ghidra.framework.plugintool.PluginTool;
24 | import ghidra.framework.plugintool.util.PluginStatus;
25 | import ghidra.program.model.listing.Program;
26 | import ghidra.util.Msg;
27 |
28 | import reva.services.RevaMcpService;
29 | import reva.ui.RevaProvider;
30 | import reva.util.RevaInternalServiceRegistry;
31 |
32 | /**
33 | * ReVa (Reverse Engineering Assistant) tool plugin for Ghidra.
34 | * This tool-level plugin connects to the application-level MCP server
35 | * and handles program lifecycle events for this specific tool.
36 | */
37 | @PluginInfo(
38 | status = PluginStatus.RELEASED,
39 | packageName = "ReVa",
40 | category = PluginCategoryNames.COMMON,
41 | shortDescription = "Reverse Engineering Assistant (Tool)",
42 | description = "Tool-level ReVa plugin that connects to the application-level MCP server"
43 | )
44 | public class RevaPlugin extends ProgramPlugin {
45 | private RevaProvider provider;
46 | private RevaMcpService mcpService;
47 |
48 | /**
49 | * Plugin constructor.
50 | * @param tool The plugin tool that this plugin is added to.
51 | */
52 | public RevaPlugin(PluginTool tool) {
53 | super(tool);
54 | Msg.info(this, "ReVa Tool Plugin initializing...");
55 |
56 | // Register this plugin in the service registry so components can access it
57 | RevaInternalServiceRegistry.registerService(RevaPlugin.class, this);
58 | }
59 |
60 | @Override
61 | public void init() {
62 | super.init();
63 |
64 | // Get the MCP service from the application plugin
65 | mcpService = tool.getService(RevaMcpService.class);
66 |
67 | // Fallback for testing environments where ApplicationLevelPlugin isn't available
68 | if (mcpService == null) {
69 | mcpService = RevaInternalServiceRegistry.getService(RevaMcpService.class);
70 | }
71 |
72 | if (mcpService == null) {
73 | Msg.error(this, "RevaMcpService not available - RevaApplicationPlugin may not be loaded and no fallback service found");
74 | return;
75 | }
76 |
77 | // Register this tool with the MCP server
78 | mcpService.registerTool(tool);
79 |
80 | // TODO: Create the UI provider when needed
81 | // provider = new RevaProvider(this, getName());
82 | // tool.addComponentProvider(provider, false);
83 |
84 | Msg.info(this, "ReVa Tool Plugin initialization complete - connected to application-level MCP server");
85 | }
86 |
87 | @Override
88 | protected void programOpened(Program program) {
89 | Msg.info(this, "Program opened: " + program.getName());
90 | // Notify the program manager to handle cache management
91 | RevaProgramManager.programOpened(program);
92 |
93 | // Notify the MCP service about the program opening in this tool
94 | if (mcpService != null) {
95 | mcpService.programOpened(program, tool);
96 | }
97 | }
98 |
99 | @Override
100 | protected void programClosed(Program program) {
101 | Msg.info(this, "Program closed: " + program.getName());
102 | // Notify the program manager to clear stale cache
103 | RevaProgramManager.programClosed(program);
104 |
105 | // Notify the MCP service about the program closing in this tool
106 | if (mcpService != null) {
107 | mcpService.programClosed(program, tool);
108 | }
109 | }
110 |
111 | @Override
112 | protected void cleanup() {
113 | // Remove the UI provider
114 | if (provider != null) {
115 | tool.removeComponentProvider(provider);
116 | }
117 |
118 | // Unregister this tool from the MCP service
119 | if (mcpService != null) {
120 | mcpService.unregisterTool(tool);
121 | }
122 |
123 | // Only clear tool-specific services, not the application-level ones
124 | RevaInternalServiceRegistry.unregisterService(RevaPlugin.class);
125 |
126 | super.cleanup();
127 | }
128 |
129 | /**
130 | * Get all currently open programs in any Ghidra tool
131 | * @return List of open programs
132 | */
133 | public List getOpenPrograms() {
134 | return RevaProgramManager.getOpenPrograms();
135 | }
136 |
137 | /**
138 | * Get the MCP service instance
139 | * @return The MCP service, or null if not available
140 | */
141 | public RevaMcpService getMcpService() {
142 | return mcpService;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/plugin/ConfigManagerSecurityTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import org.junit.Before;
21 | import org.junit.Test;
22 |
23 | import ghidra.framework.options.ToolOptions;
24 | import ghidra.framework.plugintool.PluginTool;
25 | import reva.RevaIntegrationTestBase;
26 |
27 | /**
28 | * Test class for ConfigManager security-related configuration options.
29 | */
30 | public class ConfigManagerSecurityTest extends RevaIntegrationTestBase {
31 |
32 | private ConfigManager configManager;
33 |
34 | @Before
35 | public void setUp() throws Exception {
36 | // Clear any existing configuration to start fresh
37 | ToolOptions options = tool.getOptions(ConfigManager.SERVER_OPTIONS);
38 | options.removeOption(ConfigManager.SERVER_HOST);
39 | options.removeOption(ConfigManager.API_KEY_ENABLED);
40 | options.removeOption(ConfigManager.API_KEY);
41 |
42 | // Create a fresh ConfigManager for each test to avoid state pollution
43 | // This will register default options and generate a new API key
44 | configManager = new ConfigManager(tool);
45 | }
46 |
47 | @Test
48 | public void testDefaultHostConfiguration() {
49 | // Test that the default host is localhost (secure by default)
50 | String defaultHost = configManager.getServerHost();
51 | assertEquals("Default host should be localhost for security", "127.0.0.1", defaultHost);
52 | }
53 |
54 | @Test
55 | public void testDefaultApiKeyConfiguration() {
56 | // Test that API key authentication is disabled by default
57 | boolean apiKeyEnabled = configManager.isApiKeyEnabled();
58 | assertFalse("API key authentication should be disabled by default", apiKeyEnabled);
59 |
60 | // Test that an API key is generated by default
61 | String apiKey = configManager.getApiKey();
62 | assertNotNull("API key should not be null", apiKey);
63 | assertFalse("API key should not be empty", apiKey.trim().isEmpty());
64 | assertTrue("API key should start with 'ReVa-'", apiKey.startsWith("ReVa-"));
65 | assertTrue("API key should contain a UUID", apiKey.length() > 10);
66 | }
67 |
68 | @Test
69 | public void testHostConfigurationUpdate() {
70 | // Test updating the host configuration
71 | String newHost = "0.0.0.0";
72 | configManager.setServerHost(newHost);
73 | assertEquals("Host should be updated", newHost, configManager.getServerHost());
74 | }
75 |
76 | @Test
77 | public void testApiKeyConfigurationUpdate() {
78 | // Test enabling API key authentication
79 | configManager.setApiKeyEnabled(true);
80 | assertTrue("API key authentication should be enabled", configManager.isApiKeyEnabled());
81 |
82 | // Test setting a custom API key
83 | String customApiKey = "ReVa-custom-test-key";
84 | configManager.setApiKey(customApiKey);
85 | assertEquals("Custom API key should be set", customApiKey, configManager.getApiKey());
86 | }
87 |
88 | @Test
89 | public void testApiKeyGenerationFormat() {
90 | // Test that generated API keys follow the expected format
91 | String apiKey = configManager.getApiKey();
92 | String[] parts = apiKey.split("-", 2);
93 |
94 | assertEquals("API key should start with 'ReVa'", "ReVa", parts[0]);
95 | assertEquals("API key should have UUID part", 2, parts.length);
96 | assertTrue("UUID part should be non-empty", parts[1].length() > 0);
97 | // UUID with dashes is typically 36 characters, but let's be more lenient
98 | assertTrue("UUID part should be reasonable length", parts[1].length() >= 10);
99 | }
100 |
101 | @Test
102 | public void testConfigurationPersistence() {
103 | // Test that configuration changes are persisted through the options system
104 | ToolOptions options = tool.getOptions(ConfigManager.SERVER_OPTIONS);
105 |
106 | // Set values through ConfigManager
107 | configManager.setServerHost("192.168.1.100");
108 | configManager.setApiKeyEnabled(true);
109 | configManager.setApiKey("ReVa-test-persistence");
110 |
111 | // Verify values are stored in tool options
112 | assertEquals("Host should be persisted in options",
113 | "192.168.1.100", options.getString(ConfigManager.SERVER_HOST, null));
114 | assertTrue("API key enabled should be persisted in options",
115 | options.getBoolean(ConfigManager.API_KEY_ENABLED, false));
116 | assertEquals("API key should be persisted in options",
117 | "ReVa-test-persistence", options.getString(ConfigManager.API_KEY, null));
118 | }
119 | }
--------------------------------------------------------------------------------
/src/test.slow/java/reva/tools/bookmarks/BookmarkToolProviderIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools.bookmarks;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import java.util.HashMap;
21 | import java.util.Map;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 |
26 | import com.fasterxml.jackson.databind.JsonNode;
27 |
28 | import ghidra.program.model.address.Address;
29 | import ghidra.program.model.listing.Bookmark;
30 | import ghidra.program.model.listing.BookmarkManager;
31 | import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
32 | import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
33 | import io.modelcontextprotocol.spec.McpSchema.TextContent;
34 |
35 | import reva.RevaIntegrationTestBase;
36 |
37 | /**
38 | * Integration tests for BookmarkToolProvider using MCP client.
39 | * Tests the full end-to-end flow from MCP client through the server to Ghidra.
40 | */
41 | public class BookmarkToolProviderIntegrationTest extends RevaIntegrationTestBase {
42 |
43 | private String programPath;
44 |
45 | @Before
46 | public void setUpTestData() throws Exception {
47 | programPath = program.getDomainFile().getPathname();
48 |
49 | // Open the program in the tool's ProgramManager so it can be found by RevaProgramManager
50 | env.open(program);
51 |
52 | // Also open it directly in the tool's ProgramManager service to ensure it's available
53 | ghidra.app.services.ProgramManager programManager = tool.getService(ghidra.app.services.ProgramManager.class);
54 | if (programManager != null) {
55 | programManager.openProgram(program);
56 | }
57 |
58 | // Register the program with the server manager so it can be found by the tools
59 | if (serverManager != null) {
60 | serverManager.programOpened(program, tool);
61 | }
62 | }
63 |
64 | @Test
65 | public void testSetAndGetBookmark() throws Exception {
66 | withMcpClient(createMcpTransport(), client -> {
67 | try {
68 | client.initialize();
69 |
70 | // Use the minimum address in the program which should be valid
71 | Address testAddress = program.getMinAddress();
72 | String addressStr = testAddress.toString();
73 |
74 | // Set a bookmark
75 | Map setArgs = new HashMap<>();
76 | setArgs.put("programPath", programPath);
77 | setArgs.put("addressOrSymbol", addressStr);
78 | setArgs.put("type", "Note");
79 | setArgs.put("category", "Analysis");
80 | setArgs.put("comment", "Test bookmark");
81 |
82 | CallToolRequest setRequest = new CallToolRequest("set-bookmark", setArgs);
83 | CallToolResult setResult = client.callTool(setRequest);
84 | assertFalse("Set bookmark should succeed", setResult.isError());
85 |
86 | // Verify the bookmark was set in the program
87 | BookmarkManager bookmarkMgr = program.getBookmarkManager();
88 | Bookmark bookmark = bookmarkMgr.getBookmark(testAddress, "Note", "Analysis");
89 | assertNotNull("Bookmark should exist", bookmark);
90 | assertEquals("Bookmark comment should match", "Test bookmark", bookmark.getComment());
91 |
92 | // Get the bookmark using the tool
93 | Map getArgs = new HashMap<>();
94 | getArgs.put("programPath", programPath);
95 | getArgs.put("addressOrSymbol", addressStr);
96 |
97 | CallToolRequest getRequest = new CallToolRequest("get-bookmarks", getArgs);
98 | CallToolResult getResult = client.callTool(getRequest);
99 | assertFalse("Get bookmarks should succeed", getResult.isError());
100 |
101 | // Parse the result
102 | String jsonResponse = ((TextContent) getResult.content().get(0)).text();
103 | JsonNode responseNode = objectMapper.readTree(jsonResponse);
104 | JsonNode bookmarksNode = responseNode.get("bookmarks");
105 |
106 | assertEquals("Should have one bookmark", 1, bookmarksNode.size());
107 | assertEquals("Bookmark comment should match", "Test bookmark", bookmarksNode.get(0).get("comment").asText());
108 | assertEquals("Bookmark type should match", "Note", bookmarksNode.get(0).get("type").asText());
109 | assertEquals("Bookmark category should match", "Analysis", bookmarksNode.get(0).get("category").asText());
110 | } catch (Exception e) {
111 | fail("Test failed with exception: " + e.getMessage());
112 | }
113 | });
114 | }
115 | }
--------------------------------------------------------------------------------
/src/reva_cli/launcher.py:
--------------------------------------------------------------------------------
1 | """
2 | Java ReVa launcher wrapper for Python CLI.
3 |
4 | Handles PyGhidra initialization, ReVa server startup, and project management.
5 | """
6 |
7 | import sys
8 | from typing import Optional
9 | from pathlib import Path
10 |
11 |
12 | class ReVaLauncher:
13 | """Wraps ReVa headless launcher with Python-side project management.
14 |
15 | Note: Stdio mode uses ephemeral projects in temp directories.
16 | Projects are created per-session and cleaned up on exit.
17 | """
18 |
19 | def __init__(self, config_file: Optional[Path] = None, use_random_port: bool = True):
20 | """
21 | Initialize ReVa launcher.
22 |
23 | Args:
24 | config_file: Optional configuration file path
25 | use_random_port: Whether to use random available port (default: True)
26 | """
27 | self.config_file = config_file
28 | self.use_random_port = use_random_port
29 | self.java_launcher = None
30 | self.port = None
31 | self.temp_project_dir = None
32 |
33 | def start(self) -> int:
34 | """
35 | Start ReVa headless server.
36 |
37 | Returns:
38 | Server port number
39 |
40 | Raises:
41 | RuntimeError: If server fails to start
42 | """
43 | try:
44 | # Import ReVa launcher (PyGhidra already initialized by CLI)
45 | from reva.headless import RevaHeadlessLauncher
46 | from java.io import File
47 | from .project_manager import ProjectManager
48 | import tempfile
49 |
50 | # Stdio mode: ephemeral projects in temp directory (session-scoped, auto-cleanup)
51 | # Keeps working directory clean - no .reva creation in cwd
52 | self.temp_project_dir = Path(tempfile.mkdtemp(prefix="reva_project_"))
53 | project_manager = ProjectManager()
54 | project_name = project_manager.get_project_name()
55 |
56 | # Use temp directory for the project (not .reva/projects)
57 | projects_dir = self.temp_project_dir
58 |
59 | # Convert to Java File objects
60 | java_project_location = File(str(projects_dir))
61 |
62 | print(f"Project location: {projects_dir}/{project_name}", file=sys.stderr)
63 |
64 | # Create launcher with project parameters
65 | if self.config_file:
66 | print(f"Using config file: {self.config_file}", file=sys.stderr)
67 | java_config_file = File(str(self.config_file))
68 | self.java_launcher = RevaHeadlessLauncher(
69 | java_config_file,
70 | self.use_random_port,
71 | java_project_location,
72 | project_name
73 | )
74 | else:
75 | print("Using default configuration", file=sys.stderr)
76 | # Use constructor with project parameters
77 | self.java_launcher = RevaHeadlessLauncher(
78 | None,
79 | True, # autoInitializeGhidra
80 | self.use_random_port,
81 | java_project_location,
82 | project_name
83 | )
84 |
85 | # Start server
86 | print("Starting ReVa MCP server...", file=sys.stderr)
87 | self.java_launcher.start()
88 |
89 | # Wait for server to be ready
90 | if self.java_launcher.waitForServer(30000):
91 | self.port = self.java_launcher.getPort()
92 | print(f"ReVa server ready on port {self.port}", file=sys.stderr)
93 | return self.port
94 | else:
95 | raise RuntimeError("Server failed to start within timeout")
96 |
97 | except Exception as e:
98 | print(f"Error starting ReVa server: {e}", file=sys.stderr)
99 | import traceback
100 | traceback.print_exc(file=sys.stderr)
101 | raise
102 |
103 | def get_port(self) -> Optional[int]:
104 | """
105 | Get the server port.
106 |
107 | Returns:
108 | Server port number, or None if not started
109 | """
110 | return self.port
111 |
112 | def is_running(self) -> bool:
113 | """
114 | Check if server is running.
115 |
116 | Returns:
117 | True if server is running
118 | """
119 | if self.java_launcher:
120 | return self.java_launcher.isRunning()
121 | return False
122 |
123 | def stop(self):
124 | """Stop the ReVa server and cleanup."""
125 | if self.java_launcher:
126 | print("Stopping ReVa server...", file=sys.stderr)
127 | try:
128 | self.java_launcher.stop()
129 | except Exception as e:
130 | print(f"Error stopping server: {e}", file=sys.stderr)
131 | finally:
132 | self.java_launcher = None
133 | self.port = None
134 |
135 | # Clean up temporary project directory
136 | if self.temp_project_dir and self.temp_project_dir.exists():
137 | try:
138 | import shutil
139 | shutil.rmtree(self.temp_project_dir)
140 | print(f"Cleaned up temporary project directory: {self.temp_project_dir}", file=sys.stderr)
141 | except Exception as e:
142 | print(f"Error cleaning up temporary directory: {e}", file=sys.stderr)
143 | finally:
144 | self.temp_project_dir = None
145 |
--------------------------------------------------------------------------------
/src/main/java/reva/resources/impl/ProgramListResource.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.resources.impl;
17 |
18 | import java.net.URLEncoder;
19 | import java.nio.charset.StandardCharsets;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | import com.fasterxml.jackson.core.JsonProcessingException;
24 | import com.fasterxml.jackson.databind.ObjectMapper;
25 |
26 | import ghidra.program.model.listing.Program;
27 | import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification;
28 | import io.modelcontextprotocol.server.McpSyncServer;
29 | import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
30 | import io.modelcontextprotocol.spec.McpSchema.Resource;
31 | import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
32 | import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
33 | import reva.plugin.RevaProgramManager;
34 | import reva.resources.AbstractResourceProvider;
35 |
36 | /**
37 | * Resource provider that exposes the list of currently open programs.
38 | */
39 | public class ProgramListResource extends AbstractResourceProvider {
40 | private static final ObjectMapper JSON = new ObjectMapper();
41 | private static final String RESOURCE_ID = "ghidra://programs";
42 | private static final String RESOURCE_NAME = "open-programs";
43 | private static final String RESOURCE_DESCRIPTION = "Currently open programs";
44 | private static final String RESOURCE_MIME_TYPE = "text/plain";
45 |
46 | /**
47 | * Constructor
48 | * @param server The MCP server to register with
49 | */
50 | public ProgramListResource(McpSyncServer server) {
51 | super(server);
52 | }
53 |
54 | @Override
55 | public void register() {
56 | Resource resource = new Resource(
57 | RESOURCE_ID,
58 | RESOURCE_NAME,
59 | RESOURCE_DESCRIPTION,
60 | RESOURCE_MIME_TYPE,
61 | null // No schema needed for this resource
62 | );
63 |
64 | SyncResourceSpecification resourceSpec = new SyncResourceSpecification(
65 | resource,
66 | (exchange, request) -> {
67 | List resourceContents = new ArrayList<>();
68 |
69 | // Get all open programs
70 | List openPrograms = RevaProgramManager.getOpenPrograms();
71 |
72 | for (Program program : openPrograms) {
73 | try {
74 | // Create program info object
75 | String programPath = program.getDomainFile().getPathname();
76 | String programLanguage = program.getLanguage().getLanguageID().getIdAsString();
77 | String programCompilerSpec = program.getCompilerSpec().getCompilerSpecID().getIdAsString();
78 | long programSize = program.getMemory().getSize();
79 |
80 | // Create a JSON object with program metadata
81 | String metaString = JSON.writeValueAsString(
82 | new ProgramInfo(programPath, programLanguage, programCompilerSpec, programSize)
83 | );
84 |
85 | // Add to resource contents
86 | // URL encode the program path to ensure URI safety
87 | String encodedProgramPath = URLEncoder.encode(programPath, StandardCharsets.UTF_8);
88 | resourceContents.add(
89 | new TextResourceContents(
90 | RESOURCE_ID + "/" + encodedProgramPath,
91 | RESOURCE_MIME_TYPE,
92 | metaString
93 | )
94 | );
95 | } catch (JsonProcessingException e) {
96 | logError("Error serializing program metadata", e);
97 | }
98 | }
99 |
100 | return new ReadResourceResult(resourceContents);
101 | }
102 | );
103 |
104 | server.addResource(resourceSpec);
105 | logInfo("Registered resource: " + RESOURCE_NAME);
106 | }
107 |
108 | /**
109 | * Simple class to hold program information for JSON serialization
110 | */
111 | private static class ProgramInfo {
112 | @SuppressWarnings("unused")
113 | public String programPath;
114 |
115 | @SuppressWarnings("unused")
116 | public String language;
117 |
118 | @SuppressWarnings("unused")
119 | public String compilerSpec;
120 |
121 | @SuppressWarnings("unused")
122 | public long sizeBytes;
123 |
124 | public ProgramInfo(String programPath, String language, String compilerSpec, long sizeBytes) {
125 | this.programPath = programPath;
126 | this.language = language;
127 | this.compilerSpec = compilerSpec;
128 | this.sizeBytes = sizeBytes;
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/reva/util/MemoryUtil.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.util;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 | import java.util.function.Consumer;
21 |
22 | import ghidra.program.model.address.Address;
23 | import ghidra.program.model.listing.Program;
24 | import ghidra.program.model.mem.Memory;
25 | import ghidra.program.model.mem.MemoryAccessException;
26 | import ghidra.program.model.mem.MemoryBlock;
27 |
28 | /**
29 | * Utility functions for working with Ghidra memory.
30 | */
31 | public class MemoryUtil {
32 |
33 | /**
34 | * Format a byte array as a hex string
35 | * @param bytes The byte array
36 | * @return A hex string representation
37 | */
38 | public static String formatHexString(byte[] bytes) {
39 | if (bytes == null || bytes.length == 0) {
40 | return "";
41 | }
42 |
43 | StringBuilder hexBuilder = new StringBuilder();
44 | for (byte b : bytes) {
45 | hexBuilder.append(String.format("%02X ", b & 0xFF));
46 | }
47 | return hexBuilder.toString().trim();
48 | }
49 |
50 | /**
51 | * Convert a byte array to a list of integer values (0-255)
52 | * @param bytes The byte array
53 | * @return List of integer values
54 | */
55 | public static List byteArrayToIntList(byte[] bytes) {
56 | if (bytes == null || bytes.length == 0) {
57 | return List.of();
58 | }
59 |
60 | List result = new ArrayList<>(bytes.length);
61 | for (byte b : bytes) {
62 | result.add(b & 0xFF);
63 | }
64 | return result;
65 | }
66 |
67 | /**
68 | * Read memory bytes safely
69 | * @param program The Ghidra program
70 | * @param address Starting address
71 | * @param length Number of bytes to read
72 | * @return Byte array or null if an error occurred
73 | */
74 | public static byte[] readMemoryBytes(Program program, Address address, int length) {
75 | Memory memory = program.getMemory();
76 | byte[] bytes = new byte[length];
77 |
78 | try {
79 | int read = memory.getBytes(address, bytes);
80 | if (read != length) {
81 | byte[] actualBytes = new byte[read];
82 | System.arraycopy(bytes, 0, actualBytes, 0, read);
83 | return actualBytes;
84 | }
85 | return bytes;
86 | } catch (MemoryAccessException e) {
87 | return null;
88 | }
89 | }
90 |
91 | /**
92 | * Find a memory block by name
93 | * @param program The Ghidra program
94 | * @param blockName Name of the block to find
95 | * @return The memory block or null if not found
96 | */
97 | public static MemoryBlock findBlockByName(Program program, String blockName) {
98 | Memory memory = program.getMemory();
99 | for (MemoryBlock block : memory.getBlocks()) {
100 | if (block.getName().equals(blockName)) {
101 | return block;
102 | }
103 | }
104 | return null;
105 | }
106 |
107 | /**
108 | * Find the memory block containing the given address
109 | * @param program The Ghidra program
110 | * @param address The address to look up
111 | * @return The memory block or null if not found
112 | */
113 | public static MemoryBlock getBlockContaining(Program program, Address address) {
114 | Memory memory = program.getMemory();
115 | return memory.getBlock(address);
116 | }
117 |
118 | /**
119 | * Process memory bytes in chunks to avoid large memory allocations
120 | * @param program The Ghidra program
121 | * @param startAddress Starting address
122 | * @param length Total number of bytes to process
123 | * @param chunkSize Maximum chunk size
124 | * @param processor Consumer function that processes each chunk
125 | */
126 | public static void processMemoryInChunks(
127 | Program program,
128 | Address startAddress,
129 | long length,
130 | int chunkSize,
131 | Consumer processor) {
132 |
133 | Memory memory = program.getMemory();
134 | Address currentAddress = startAddress;
135 | long remaining = length;
136 |
137 | while (remaining > 0) {
138 | int currentChunkSize = (int) Math.min(remaining, chunkSize);
139 | byte[] buffer = new byte[currentChunkSize];
140 |
141 | try {
142 | int read = memory.getBytes(currentAddress, buffer);
143 | if (read > 0) {
144 | processor.accept(buffer);
145 | currentAddress = currentAddress.add(read);
146 | remaining -= read;
147 | } else {
148 | break; // Could not read any bytes
149 | }
150 | } catch (MemoryAccessException e) {
151 | break; // Memory access error
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/test/java/reva/tools/strings/StringToolProviderTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools.strings;
17 |
18 | import static org.junit.Assert.*;
19 | import static org.mockito.Mockito.*;
20 |
21 | import java.util.Map;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.mockito.Mock;
26 | import org.mockito.MockitoAnnotations;
27 |
28 | import ghidra.program.model.address.Address;
29 | import ghidra.program.model.data.DataType;
30 | import ghidra.program.model.listing.Data;
31 | import ghidra.program.model.listing.DataIterator;
32 | import ghidra.program.model.listing.Listing;
33 | import ghidra.program.model.listing.Program;
34 | import io.modelcontextprotocol.server.McpSyncServer;
35 | import io.modelcontextprotocol.spec.McpError;
36 |
37 | /**
38 | * Unit tests for StringToolProvider
39 | */
40 | public class StringToolProviderTest {
41 | @Mock
42 | private McpSyncServer mockServer;
43 |
44 | @Mock
45 | private Program mockProgram;
46 |
47 | @Mock
48 | private Listing mockListing;
49 |
50 | @Mock
51 | private DataIterator mockDataIterator;
52 |
53 | @Mock
54 | private Data mockData;
55 |
56 | @Mock
57 | private Address mockAddress;
58 |
59 | @Mock
60 | private DataType mockDataType;
61 |
62 | private StringToolProvider stringToolProvider;
63 |
64 | @Before
65 | public void setUp() {
66 | MockitoAnnotations.openMocks(this);
67 | stringToolProvider = new StringToolProvider(mockServer);
68 | }
69 |
70 | @Test
71 | public void testConstructor() {
72 | assertNotNull("StringToolProvider should be created", stringToolProvider);
73 | }
74 |
75 | @Test
76 | public void testRegisterTools() throws McpError {
77 | // Test that registerTools completes without throwing
78 | stringToolProvider.registerTools();
79 | }
80 |
81 | @Test
82 | public void testGetStringInfoWithValidString() throws Exception {
83 | // Setup mock data
84 | String testString = "Hello, World!";
85 | byte[] testBytes = testString.getBytes();
86 |
87 | when(mockData.getValue()).thenReturn(testString);
88 | when(mockData.getAddress()).thenReturn(mockAddress);
89 | when(mockAddress.toString()).thenReturn("00401000");
90 | when(mockAddress.toString("0x")).thenReturn("0x00401000");
91 | when(mockData.getBytes()).thenReturn(testBytes);
92 | when(mockData.getDataType()).thenReturn(mockDataType);
93 | when(mockDataType.getName()).thenReturn("string");
94 | when(mockData.getDefaultValueRepresentation()).thenReturn("\"Hello, World!\"");
95 |
96 | // Use reflection to test the private method
97 | java.lang.reflect.Method method = StringToolProvider.class.getDeclaredMethod("getStringInfo", Data.class);
98 | method.setAccessible(true);
99 |
100 | @SuppressWarnings("unchecked")
101 | Map result = (Map) method.invoke(stringToolProvider, mockData);
102 |
103 | assertNotNull("Result should not be null", result);
104 | assertEquals("Address should match", "0x00401000", result.get("address"));
105 | assertEquals("Content should match", testString, result.get("content"));
106 | assertEquals("Length should match", testString.length(), result.get("length"));
107 | assertEquals("Data type should match", "string", result.get("dataType"));
108 | assertEquals("Representation should match", "\"Hello, World!\"", result.get("representation"));
109 | assertNotNull("Hex bytes should be present", result.get("hexBytes"));
110 | assertEquals("Byte length should match", testBytes.length, result.get("byteLength"));
111 | }
112 |
113 | @Test
114 | public void testGetStringInfoWithNonString() throws Exception {
115 | // Setup mock data with non-string value
116 | when(mockData.getValue()).thenReturn(Integer.valueOf(42));
117 |
118 | // Use reflection to test the private method
119 | java.lang.reflect.Method method = StringToolProvider.class.getDeclaredMethod("getStringInfo", Data.class);
120 | method.setAccessible(true);
121 |
122 | @SuppressWarnings("unchecked")
123 | Map result = (Map) method.invoke(stringToolProvider, mockData);
124 |
125 | assertNull("Result should be null for non-string data", result);
126 | }
127 |
128 | @Test
129 | public void testInheritance() {
130 | // Test that StringToolProvider extends AbstractToolProvider
131 | assertTrue("StringToolProvider should extend AbstractToolProvider",
132 | reva.tools.AbstractToolProvider.class.isAssignableFrom(StringToolProvider.class));
133 | }
134 |
135 | @Test
136 | public void testToolProviderInterface() {
137 | // Test that StringToolProvider implements ToolProvider interface
138 | assertTrue("StringToolProvider should implement ToolProvider",
139 | reva.tools.ToolProvider.class.isAssignableFrom(StringToolProvider.class));
140 | }
141 | }
--------------------------------------------------------------------------------
/src/reva_cli/stdio_bridge.py:
--------------------------------------------------------------------------------
1 | """
2 | Stdio to HTTP MCP bridge using official MCP SDK Server abstraction.
3 |
4 | Provides a proper MCP Server that forwards all requests to ReVa's StreamableHTTP endpoint.
5 | Uses the MCP SDK's stdio transport and Pydantic serialization - no manual JSON-RPC handling.
6 | """
7 |
8 | import sys
9 | from typing import Any
10 |
11 | from mcp.server import Server
12 | from mcp.server.stdio import stdio_server
13 | from mcp.client.streamable_http import streamablehttp_client
14 | from mcp import ClientSession
15 | from mcp.types import (
16 | Tool,
17 | Resource,
18 | Prompt,
19 | TextContent,
20 | ImageContent,
21 | EmbeddedResource,
22 | )
23 |
24 |
25 | class ReVaStdioBridge:
26 | """
27 | MCP Server that bridges stdio to ReVa's StreamableHTTP endpoint.
28 |
29 | Acts as a transparent proxy - forwards all MCP requests to the ReVa backend
30 | and returns responses. The MCP SDK handles all JSON-RPC serialization.
31 | """
32 |
33 | def __init__(self, port: int):
34 | """
35 | Initialize the stdio bridge.
36 |
37 | Args:
38 | port: ReVa server port to connect to
39 | """
40 | self.port = port
41 | self.url = f"http://localhost:{port}/mcp/message"
42 | self.server = Server("ReVa")
43 | self.backend_session: ClientSession | None = None
44 |
45 | # Register handlers
46 | self._register_handlers()
47 |
48 | def _register_handlers(self):
49 | """Register MCP protocol handlers that forward to ReVa backend."""
50 |
51 | @self.server.list_tools()
52 | async def list_tools() -> list[Tool]:
53 | """Forward list_tools request to ReVa backend."""
54 | if not self.backend_session:
55 | raise RuntimeError("Backend session not initialized")
56 |
57 | result = await self.backend_session.list_tools()
58 | return result.tools
59 |
60 | @self.server.call_tool()
61 | async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent | ImageContent | EmbeddedResource]:
62 | """Forward call_tool request to ReVa backend."""
63 | if not self.backend_session:
64 | raise RuntimeError("Backend session not initialized")
65 |
66 | result = await self.backend_session.call_tool(name, arguments)
67 | return result.content
68 |
69 | @self.server.list_resources()
70 | async def list_resources() -> list[Resource]:
71 | """Forward list_resources request to ReVa backend."""
72 | if not self.backend_session:
73 | raise RuntimeError("Backend session not initialized")
74 |
75 | result = await self.backend_session.list_resources()
76 | return result.resources
77 |
78 | @self.server.read_resource()
79 | async def read_resource(uri: str) -> str | bytes:
80 | """Forward read_resource request to ReVa backend."""
81 | if not self.backend_session:
82 | raise RuntimeError("Backend session not initialized")
83 |
84 | result = await self.backend_session.read_resource(uri)
85 | # Return the first content item's text or blob
86 | if result.contents and len(result.contents) > 0:
87 | content = result.contents[0]
88 | if hasattr(content, 'text') and content.text:
89 | return content.text
90 | elif hasattr(content, 'blob') and content.blob:
91 | return content.blob
92 | return ""
93 |
94 | @self.server.list_prompts()
95 | async def list_prompts() -> list[Prompt]:
96 | """Forward list_prompts request to ReVa backend."""
97 | if not self.backend_session:
98 | raise RuntimeError("Backend session not initialized")
99 |
100 | result = await self.backend_session.list_prompts()
101 | return result.prompts
102 |
103 | async def run(self):
104 | """
105 | Run the stdio bridge.
106 |
107 | Connects to ReVa backend via StreamableHTTP, initializes the session,
108 | then exposes the MCP server via stdio transport.
109 | """
110 | print(f"Connecting to ReVa backend at {self.url}...", file=sys.stderr)
111 |
112 | try:
113 | # Connect to ReVa backend
114 | async with streamablehttp_client(self.url, timeout=300.0) as (read_stream, write_stream, get_session_id):
115 | async with ClientSession(read_stream, write_stream) as session:
116 | self.backend_session = session
117 |
118 | # Initialize backend session
119 | print("Initializing ReVa backend session...", file=sys.stderr)
120 | init_result = await session.initialize()
121 | print(f"Connected to {init_result.serverInfo.name} v{init_result.serverInfo.version}", file=sys.stderr)
122 |
123 | # Run MCP server with stdio transport
124 | print("Bridge ready - stdio transport active", file=sys.stderr)
125 | async with stdio_server() as (read_stream, write_stream):
126 | await self.server.run(
127 | read_stream,
128 | write_stream,
129 | self.server.create_initialization_options()
130 | )
131 |
132 | except Exception as e:
133 | print(f"Bridge error: {e}", file=sys.stderr)
134 | import traceback
135 | traceback.print_exc(file=sys.stderr)
136 | raise
137 | finally:
138 | self.backend_session = None
139 | print("Bridge stopped", file=sys.stderr)
140 |
141 | def stop(self):
142 | """Stop the bridge (handled by context managers)."""
143 | # Cleanup is handled by async context managers
144 | pass
145 |
--------------------------------------------------------------------------------
/src/test.slow/java/reva/tools/data/DataToolProviderIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.tools.data;
17 |
18 | import static org.junit.Assert.*;
19 |
20 | import org.junit.Before;
21 | import org.junit.Test;
22 |
23 | import ghidra.program.model.address.Address;
24 | import ghidra.program.model.data.ByteDataType;
25 | import ghidra.program.model.data.IntegerDataType;
26 | import ghidra.program.model.listing.Data;
27 | import ghidra.program.model.listing.Listing;
28 | import ghidra.program.model.symbol.SymbolTable;
29 | import reva.RevaIntegrationTestBase;
30 |
31 | /**
32 | * Integration tests for DataToolProvider that test actual MCP tool calls
33 | * with real program data.
34 | */
35 | public class DataToolProviderIntegrationTest extends RevaIntegrationTestBase {
36 |
37 | private String programPath;
38 | private Address testAddress1;
39 | private Address testAddress2;
40 |
41 | @Before
42 | public void setUpTestData() throws Exception {
43 | programPath = program.getDomainFile().getPathname();
44 |
45 | // Set up test data in the program
46 | int txId = program.startTransaction("Setup test data");
47 | try {
48 | Listing listing = program.getListing();
49 | SymbolTable symbolTable = program.getSymbolTable();
50 |
51 | // Create test addresses
52 | testAddress1 = program.getAddressFactory().getDefaultAddressSpace().getAddress(0x01000100);
53 | testAddress2 = program.getAddressFactory().getDefaultAddressSpace().getAddress(0x01000200);
54 |
55 | // Create some test data at testAddress1 (integer)
56 | listing.createData(testAddress1, new IntegerDataType(), 4);
57 |
58 | // Create test data at testAddress2 (bytes)
59 | listing.createData(testAddress2, new ByteDataType(), 1);
60 | listing.createData(testAddress2.add(1), new ByteDataType(), 1);
61 | listing.createData(testAddress2.add(2), new ByteDataType(), 1);
62 | listing.createData(testAddress2.add(3), new ByteDataType(), 1);
63 |
64 | // Create test symbols
65 | symbolTable.createLabel(testAddress1, "test_int_symbol",
66 | program.getGlobalNamespace(),
67 | ghidra.program.model.symbol.SourceType.USER_DEFINED);
68 | symbolTable.createLabel(testAddress2, "test_byte_symbol",
69 | program.getGlobalNamespace(),
70 | ghidra.program.model.symbol.SourceType.USER_DEFINED);
71 |
72 | } finally {
73 | program.endTransaction(txId, true);
74 | }
75 | }
76 |
77 | @Test
78 | public void testDataSetupAndToolRegistration() throws Exception {
79 | // Verify the test data was set up correctly
80 | Listing listing = program.getListing();
81 |
82 | // Check that data exists at our test addresses
83 | Data data1 = listing.getDataAt(testAddress1);
84 | assertNotNull("Data should exist at testAddress1", data1);
85 | assertEquals("Data type should be int", "int", data1.getDataType().getName());
86 |
87 | Data data2 = listing.getDataAt(testAddress2);
88 | assertNotNull("Data should exist at testAddress2", data2);
89 | assertEquals("Data type should be byte", "byte", data2.getDataType().getName());
90 |
91 | // Check that symbols were created
92 | SymbolTable symbolTable = program.getSymbolTable();
93 | var symbols1 = symbolTable.getLabelOrFunctionSymbols("test_int_symbol", null);
94 | assertFalse("Symbol test_int_symbol should exist", symbols1.isEmpty());
95 | assertEquals("Symbol should be at correct address", testAddress1, symbols1.get(0).getAddress());
96 |
97 | var symbols2 = symbolTable.getLabelOrFunctionSymbols("test_byte_symbol", null);
98 | assertFalse("Symbol test_byte_symbol should exist", symbols2.isEmpty());
99 | assertEquals("Symbol should be at correct address", testAddress2, symbols2.get(0).getAddress());
100 |
101 | // Verify that the MCP server has the DataToolProvider tools registered
102 | // We can check this by looking at the server's registered tools
103 | io.modelcontextprotocol.server.McpSyncServer mcpServer =
104 | reva.util.RevaInternalServiceRegistry.getService(io.modelcontextprotocol.server.McpSyncServer.class);
105 | assertNotNull("MCP server should be registered", mcpServer);
106 |
107 | // The tools should be registered and the server should be running
108 | // This validates that our tool provider integration is working
109 | }
110 |
111 | @Test
112 | public void testProgramSetupIsCorrect() throws Exception {
113 | // Verify that the program path is set correctly
114 | assertNotNull("Program path should be set", programPath);
115 | assertNotNull("Program should be set", program);
116 |
117 | // Verify the config manager and server port are available
118 | assertNotNull("Config manager should be available", configManager);
119 | assertEquals("Server port should be 8080", 8080, configManager.getServerPort());
120 |
121 | // Verify that addresses are in the expected memory space
122 | assertEquals("Test address 1 should be in default space",
123 | program.getAddressFactory().getDefaultAddressSpace(),
124 | testAddress1.getAddressSpace());
125 | assertEquals("Test address 2 should be in default space",
126 | program.getAddressFactory().getDefaultAddressSpace(),
127 | testAddress2.getAddressSpace());
128 | }
129 | }
--------------------------------------------------------------------------------
/src/main/java/reva/revaFileSystem.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva;
17 |
18 | import java.io.IOException;
19 | import java.util.Comparator;
20 | import java.util.List;
21 |
22 | import ghidra.app.util.bin.ByteProvider;
23 | import ghidra.app.util.bin.ByteProviderWrapper;
24 | import ghidra.formats.gfilesystem.*;
25 | import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
26 | import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
27 | import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider;
28 | import ghidra.formats.gfilesystem.fileinfo.FileAttributeType;
29 | import ghidra.formats.gfilesystem.fileinfo.FileAttributes;
30 | import ghidra.util.exception.CancelledException;
31 | import ghidra.util.task.TaskMonitor;
32 |
33 | /**
34 | * Provide class-level documentation that describes what this file system does.
35 | */
36 | @FileSystemInfo(type = "fstypegoeshere", // ([a-z0-9]+ only)
37 | description = "File system description goes here", factory = revaFileSystem.MyFileSystemFactory.class)
38 | public class revaFileSystem implements GFileSystem {
39 |
40 | private final FSRLRoot fsFSRL;
41 | private FileSystemIndexHelper fsih;
42 | private FileSystemRefManager refManager = new FileSystemRefManager(this);
43 |
44 | private ByteProvider provider;
45 |
46 | /**
47 | * File system constructor.
48 | *
49 | * @param fsFSRL The root {@link FSRL} of the file system.
50 | * @param provider The file system provider.
51 | */
52 | public revaFileSystem(FSRLRoot fsFSRL, ByteProvider provider) {
53 | this.fsFSRL = fsFSRL;
54 | this.provider = provider;
55 | this.fsih = new FileSystemIndexHelper<>(this, fsFSRL);
56 | }
57 |
58 | /**
59 | * Mounts (opens) the file system.
60 | *
61 | * @param monitor A cancellable task monitor.
62 | */
63 | public void mount(TaskMonitor monitor) {
64 | monitor.setMessage("Opening " + revaFileSystem.class.getSimpleName() + "...");
65 |
66 | // Customize how things in the file system are stored. The following should be
67 | // treated as pseudo-code.
68 | for (MyMetadata metadata : new MyMetadata[10]) {
69 | if (monitor.isCancelled()) {
70 | break;
71 | }
72 | fsih.storeFile(metadata.path, fsih.getFileCount(), false, metadata.size, metadata);
73 | }
74 | }
75 |
76 | @Override
77 | public void close() throws IOException {
78 | refManager.onClose();
79 | if (provider != null) {
80 | provider.close();
81 | provider = null;
82 | }
83 | fsih.clear();
84 | }
85 |
86 | @Override
87 | public String getName() {
88 | return fsFSRL.getContainer().getName();
89 | }
90 |
91 | @Override
92 | public FSRLRoot getFSRL() {
93 | return fsFSRL;
94 | }
95 |
96 | @Override
97 | public boolean isClosed() {
98 | return provider == null;
99 | }
100 |
101 | @Override
102 | public int getFileCount() {
103 | return fsih.getFileCount();
104 | }
105 |
106 | @Override
107 | public FileSystemRefManager getRefManager() {
108 | return refManager;
109 | }
110 |
111 | @Override
112 | public GFile lookup(String path) throws IOException {
113 | return fsih.lookup(path);
114 | }
115 |
116 | @Override
117 | public GFile lookup(String path, Comparator nameComp) throws IOException {
118 | return fsih.lookup(null, path, nameComp);
119 | }
120 |
121 | @Override
122 | public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
123 | throws IOException, CancelledException {
124 |
125 | // Get an ByteProvider for a file. The following is an example of how the metadata
126 | // might be used to get an sub-ByteProvider from a stored provider offset.
127 | MyMetadata metadata = fsih.getMetadata(file);
128 | return (metadata != null)
129 | ? new ByteProviderWrapper(provider, metadata.offset, metadata.size, file.getFSRL())
130 | : null;
131 | }
132 |
133 | @Override
134 | public List getListing(GFile directory) throws IOException {
135 | return fsih.getListing(directory);
136 | }
137 |
138 | @Override
139 | public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
140 | MyMetadata metadata = fsih.getMetadata(file);
141 | FileAttributes result = new FileAttributes();
142 | if (metadata != null) {
143 | result.add(FileAttributeType.NAME_ATTR, metadata.name);
144 | result.add(FileAttributeType.SIZE_ATTR, metadata.size);
145 | }
146 | return result;
147 | }
148 |
149 | // Customize for the real file system.
150 | public static class MyFileSystemFactory
151 | implements GFileSystemFactoryByteProvider,
152 | GFileSystemProbeByteProvider {
153 |
154 | @Override
155 | public revaFileSystem create(FSRLRoot targetFSRL,
156 | ByteProvider byteProvider, FileSystemService fsService, TaskMonitor monitor)
157 | throws IOException, CancelledException {
158 |
159 | revaFileSystem fs = new revaFileSystem(targetFSRL, byteProvider);
160 | fs.mount(monitor);
161 | return fs;
162 | }
163 |
164 | @Override
165 | public boolean probe(ByteProvider byteProvider, FileSystemService fsService,
166 | TaskMonitor monitor) throws IOException, CancelledException {
167 |
168 | // Quickly and efficiently examine the bytes in 'byteProvider' to determine if
169 | // it's a valid file system. If it is, return true.
170 |
171 | return false;
172 | }
173 | }
174 |
175 | // Customize with metadata from files in the real file system. This is just a stub.
176 | // The elements of the file system will most likely be modeled by Java classes external to this
177 | // file.
178 | private static class MyMetadata {
179 | private String name;
180 | private String path;
181 | private long offset;
182 | private long size;
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/main/java/reva/plugin/RevaApplicationPlugin.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.plugin;
17 |
18 | import ghidra.app.plugin.PluginCategoryNames;
19 | import ghidra.framework.main.ApplicationLevelOnlyPlugin;
20 | import ghidra.framework.main.FrontEndService;
21 | import ghidra.framework.model.Project;
22 | import ghidra.framework.model.ProjectListener;
23 | import ghidra.framework.plugintool.Plugin;
24 | import ghidra.framework.plugintool.PluginInfo;
25 | import ghidra.framework.plugintool.PluginTool;
26 | import ghidra.framework.plugintool.util.PluginStatus;
27 | import ghidra.framework.ShutdownHookRegistry;
28 | import ghidra.framework.ShutdownPriority;
29 | import ghidra.util.Msg;
30 |
31 | import reva.server.McpServerManager;
32 | import reva.services.RevaMcpService;
33 | import reva.util.RevaInternalServiceRegistry;
34 |
35 | /**
36 | * Application-level ReVa plugin that manages the MCP server at Ghidra application level.
37 | * This plugin persists across tool sessions and ensures the MCP server remains
38 | * running even when individual analysis tools are closed and reopened.
39 | */
40 | @PluginInfo(
41 | status = PluginStatus.RELEASED,
42 | packageName = "ReVa",
43 | category = PluginCategoryNames.COMMON,
44 | shortDescription = "ReVa Application Manager",
45 | description = "Manages the ReVa MCP server at the Ghidra application level",
46 | servicesProvided = { RevaMcpService.class },
47 | servicesRequired = { FrontEndService.class }
48 | )
49 | public class RevaApplicationPlugin extends Plugin implements ApplicationLevelOnlyPlugin, ProjectListener {
50 | private McpServerManager serverManager;
51 | private FrontEndService frontEndService;
52 | private Project currentProject;
53 |
54 | /**
55 | * Plugin constructor.
56 | * @param tool The FrontEndTool that this plugin is added to
57 | */
58 | public RevaApplicationPlugin(PluginTool tool) {
59 | super(tool);
60 | Msg.info(this, "ReVa Application Plugin initializing...");
61 | }
62 |
63 | @Override
64 | protected void init() {
65 | super.init();
66 |
67 | // Initialize the MCP server manager
68 | serverManager = new McpServerManager(tool);
69 |
70 | // Register the service
71 | registerServiceProvided(RevaMcpService.class, serverManager);
72 |
73 | // Make server manager available via service registry for backward compatibility
74 | RevaInternalServiceRegistry.registerService(McpServerManager.class, serverManager);
75 | RevaInternalServiceRegistry.registerService(RevaMcpService.class, serverManager);
76 |
77 | // Start the MCP server
78 | serverManager.startServer();
79 |
80 | // Register for project events using FrontEndService
81 | frontEndService = tool.getService(FrontEndService.class);
82 | if (frontEndService != null) {
83 | frontEndService.addProjectListener(this);
84 | }
85 |
86 | // Check if there's already an active project
87 | Project activeProject = tool.getProjectManager().getActiveProject();
88 | if (activeProject != null) {
89 | projectOpened(activeProject);
90 | }
91 |
92 | // Register shutdown hook for clean server shutdown
93 | ShutdownHookRegistry.addShutdownHook(
94 | () -> {
95 | if (serverManager != null) {
96 | serverManager.shutdown();
97 | }
98 | },
99 | ShutdownPriority.FIRST.after()
100 | );
101 |
102 | Msg.info(this, "ReVa Application Plugin initialization complete - MCP server running at application level");
103 | }
104 |
105 | @Override
106 | protected void dispose() {
107 | Msg.info(this, "ReVa Application Plugin disposing...");
108 |
109 | // Remove project listener
110 | if (frontEndService != null) {
111 | frontEndService.removeProjectListener(this);
112 | frontEndService = null;
113 | }
114 |
115 | // Clean up any active project state
116 | Project activeProject = tool.getProjectManager().getActiveProject();
117 | if (activeProject != null) {
118 | projectClosed(activeProject);
119 | }
120 |
121 | // Shutdown the MCP server
122 | if (serverManager != null) {
123 | serverManager.shutdown();
124 | serverManager = null;
125 | }
126 |
127 | // Clear service registry
128 | RevaInternalServiceRegistry.clearAllServices();
129 |
130 | super.dispose();
131 | Msg.info(this, "ReVa Application Plugin disposed");
132 | }
133 |
134 | @Override
135 | public void projectOpened(Project project) {
136 | this.currentProject = project;
137 | Msg.info(this, "Project opened: " + project.getName());
138 | // The MCP server doesn't need to restart - it continues serving across projects
139 | }
140 |
141 | @Override
142 | public void projectClosed(Project project) {
143 | if (this.currentProject == project) {
144 | this.currentProject = null;
145 | }
146 | Msg.info(this, "Project closed: " + project.getName());
147 | // The MCP server continues running even when projects are closed
148 | }
149 |
150 | /**
151 | * Get the current project
152 | * @return The currently open project, or null if no project is open
153 | */
154 | public Project getCurrentProject() {
155 | return currentProject;
156 | }
157 |
158 | /**
159 | * Get the MCP server manager
160 | * @return The server manager instance
161 | */
162 | public RevaMcpService getMcpService() {
163 | return serverManager;
164 | }
165 | }
--------------------------------------------------------------------------------
/src/main/java/reva/server/ApiKeyAuthFilter.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package reva.server;
17 |
18 | import java.io.IOException;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 |
22 | import jakarta.servlet.Filter;
23 | import jakarta.servlet.FilterChain;
24 | import jakarta.servlet.FilterConfig;
25 | import jakarta.servlet.ServletException;
26 | import jakarta.servlet.ServletRequest;
27 | import jakarta.servlet.ServletResponse;
28 | import jakarta.servlet.http.HttpServletRequest;
29 | import jakarta.servlet.http.HttpServletResponse;
30 |
31 | import com.fasterxml.jackson.databind.ObjectMapper;
32 |
33 | import ghidra.util.Msg;
34 |
35 | import reva.plugin.ConfigManager;
36 |
37 | /**
38 | * Authentication filter for API key-based access control to the MCP server.
39 | * Checks for the X-API-Key header when authentication is enabled in configuration.
40 | */
41 | public class ApiKeyAuthFilter implements Filter {
42 | private static final String API_KEY_HEADER = "X-API-Key";
43 | private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
44 |
45 | private final ConfigManager configManager;
46 |
47 | /**
48 | * Constructor
49 | * @param configManager The configuration manager to get API key settings from
50 | */
51 | public ApiKeyAuthFilter(ConfigManager configManager) {
52 | this.configManager = configManager;
53 | }
54 |
55 | @Override
56 | public void init(FilterConfig filterConfig) throws ServletException {
57 | // No initialization needed
58 | }
59 |
60 | @Override
61 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
62 | throws IOException, ServletException {
63 |
64 | // Only process HTTP requests
65 | if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
66 | chain.doFilter(request, response);
67 | return;
68 | }
69 |
70 | HttpServletRequest httpRequest = (HttpServletRequest) request;
71 | HttpServletResponse httpResponse = (HttpServletResponse) response;
72 |
73 | // Check if API key authentication is enabled
74 | if (!configManager.isApiKeyEnabled()) {
75 | // Authentication disabled - allow all requests
76 | chain.doFilter(request, response);
77 | return;
78 | }
79 |
80 | // Get the API key from the request header
81 | String providedApiKey = httpRequest.getHeader(API_KEY_HEADER);
82 | String configuredApiKey = configManager.getApiKey();
83 |
84 | // Validate API key
85 | if (providedApiKey == null || providedApiKey.trim().isEmpty()) {
86 | Msg.warn(this, "API key authentication failed: missing X-API-Key header from " +
87 | getClientInfo(httpRequest));
88 | sendUnauthorizedResponse(httpResponse, "Missing X-API-Key header");
89 | return;
90 | }
91 |
92 | if (configuredApiKey == null || configuredApiKey.trim().isEmpty()) {
93 | Msg.error(this, "API key authentication failed: no API key configured in settings");
94 | sendUnauthorizedResponse(httpResponse, "Server configuration error");
95 | return;
96 | }
97 |
98 | if (!providedApiKey.equals(configuredApiKey)) {
99 | Msg.warn(this, "API key authentication failed: invalid API key from " +
100 | getClientInfo(httpRequest));
101 | sendUnauthorizedResponse(httpResponse, "Invalid API key");
102 | return;
103 | }
104 |
105 | // API key is valid - allow the request to continue
106 | Msg.debug(this, "API key authentication successful for " + getClientInfo(httpRequest));
107 | chain.doFilter(request, response);
108 | }
109 |
110 | @Override
111 | public void destroy() {
112 | // No cleanup needed
113 | }
114 |
115 | /**
116 | * Send an HTTP 401 Unauthorized response
117 | * @param response The HTTP response to modify
118 | * @param message The error message to include
119 | * @throws IOException If writing the response fails
120 | */
121 | private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
122 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
123 | response.setContentType("application/json");
124 |
125 | // Use Jackson to properly serialize JSON and prevent injection
126 | Map errorResponse = new HashMap<>();
127 | errorResponse.put("error", "Unauthorized");
128 | errorResponse.put("message", message);
129 |
130 | response.getWriter().write(JSON_MAPPER.writeValueAsString(errorResponse));
131 | }
132 |
133 | /**
134 | * Get client information for logging
135 | * @param request The HTTP request
136 | * @return A string with client IP and user agent
137 | */
138 | private String getClientInfo(HttpServletRequest request) {
139 | String clientIP = null;
140 | String xForwardedFor = request.getHeader("X-Forwarded-For");
141 | if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
142 | // X-Forwarded-For can contain multiple IPs, the first is the original client
143 | clientIP = xForwardedFor.split(",")[0].trim();
144 | } else {
145 | String xRealIp = request.getHeader("X-Real-IP");
146 | if (xRealIp != null && !xRealIp.isEmpty()) {
147 | clientIP = xRealIp;
148 | } else {
149 | clientIP = request.getRemoteAddr();
150 | }
151 | }
152 | String userAgent = request.getHeader("User-Agent");
153 | return clientIP + (userAgent != null ? " (" + userAgent + ")" : "");
154 | }
155 | }
--------------------------------------------------------------------------------
/data/languages/skel.sinc:
--------------------------------------------------------------------------------
1 | # sleigh include file for Skeleton language instructions
2 |
3 | define token opbyte (8)
4 | op0_8 = (0,7)
5 | op6_2 = (6,7)
6 |
7 | dRegPair4_2 = (4,5)
8 | pRegPair4_2 = (4,5)
9 | sRegPair4_2 = (4,5)
10 | qRegPair4_2 = (4,5)
11 | qRegPair4_2a = (4,5)
12 | qRegPair4_2b = (4,5)
13 | rRegPair4_2 = (4,5)
14 |
15 | reg3_3 = (3,5)
16 | bits3_3 = (3,5)
17 |
18 | bits0_4 = (0,3)
19 |
20 | reg0_3 = (0,2)
21 | bits0_3 = (0,2)
22 | ;
23 |
24 | define token data8 (8)
25 | imm8 = (0,7)
26 | sign8 = (7,7)
27 | simm8 = (0,7) signed
28 | ;
29 |
30 | define token data16 (16)
31 | timm4 = (12,15)
32 | imm16 = (0,15)
33 | sign16 = (15,15)
34 | simm16 = (0,15) signed
35 | ;
36 |
37 | attach variables [ reg0_3 reg3_3 ] [ B C D E H L _ A ];
38 |
39 | attach variables [ sRegPair4_2 dRegPair4_2 ] [ BC DE HL SP ];
40 |
41 | attach variables [ qRegPair4_2 ] [ BC DE HL AF ];
42 | attach variables [ qRegPair4_2a ] [ B D H A ];
43 | attach variables [ qRegPair4_2b ] [ C E L F ];
44 |
45 | attach variables [ pRegPair4_2 ] [ BC DE IX SP ];
46 | attach variables [ rRegPair4_2 ] [ BC DE IY SP ];
47 |
48 | ################################################################
49 | # Macros
50 | ################################################################
51 |
52 | macro setResultFlags(result) {
53 | $(Z_flag) = (result == 0);
54 | $(S_flag) = (result s< 0);
55 | }
56 |
57 | macro setAddCarryFlags(op1,op2) {
58 | $(C_flag) = (carry(op1,zext($(C_flag))) || carry(op2,op1 + zext($(C_flag))));
59 | }
60 |
61 | macro setAddFlags(op1,op2) {
62 | $(C_flag) = carry(op1,op2);
63 | }
64 |
65 | macro setSubtractCarryFlags(op1,op2) {
66 | notC = ~$(C_flag);
67 | $(C_flag) = ((op1 < sext(notC)) || (op2 < (op1 - sext(notC))));
68 | }
69 |
70 | macro setSubtractFlags(op1,op2) {
71 | $(C_flag) = (op1 < op2);
72 | }
73 |
74 | macro push16(val16) {
75 | SP = SP - 2;
76 | *:2 SP = val16;
77 | }
78 |
79 | macro pop16(ret16) {
80 | ret16 = *:2 SP;
81 | SP = SP + 2;
82 | }
83 |
84 | macro push8(val8) {
85 | SP = SP - 1;
86 | ptr:2 = SP;
87 | *:1 ptr = val8;
88 | }
89 |
90 | macro pop8(ret8) {
91 | ptr:2 = SP;
92 | ret8 = *:1 ptr;
93 | SP = SP + 1;
94 | }
95 |
96 | ################################################################
97 |
98 | ixMem8: (IX+simm8) is IX & simm8 { ptr:2 = IX + simm8; export *:1 ptr; }
99 | ixMem8: (IX-val) is IX & simm8 & sign8=1 [ val = -simm8; ] { ptr:2 = IX + simm8; export *:1 ptr; }
100 |
101 | iyMem8: (IY+simm8) is IY & simm8 { ptr:2 = IY + simm8; export *:1 ptr; }
102 | iyMem8: (IY-val) is IY & simm8 & sign8=1 [ val = -simm8; ] { ptr:2 = IY + simm8; export *:1 ptr; }
103 |
104 | Addr16: imm16 is imm16 { export *:1 imm16; }
105 |
106 | Mem16: (imm16) is imm16 { export *:2 imm16; }
107 |
108 | RelAddr8: loc is simm8 [ loc = inst_next + simm8; ] { export *:1 loc; }
109 |
110 | cc: "NZ" is bits3_3=0x0 { c:1 = ($(Z_flag) == 0); export c; }
111 | cc: "Z" is bits3_3=0x1 { c:1 = $(Z_flag); export c; }
112 | cc: "NC" is bits3_3=0x2 { c:1 = ($(C_flag) == 0); export c; }
113 | cc: "C" is bits3_3=0x3 { c:1 = $(C_flag); export c; }
114 | cc: "PO" is bits3_3=0x4 { c:1 = ($(PV_flag) == 0); export c; }
115 | cc: "PE" is bits3_3=0x5 { c:1 = $(PV_flag); export c; }
116 | cc: "P" is bits3_3=0x6 { c:1 = ($(S_flag) == 0); export c; }
117 | cc: "M" is bits3_3=0x7 { c:1 = $(S_flag); export c; }
118 |
119 | cc2: "NZ" is bits3_3=0x4 { c:1 = ($(Z_flag) == 0); export c; }
120 | cc2: "Z" is bits3_3=0x5 { c:1 = $(Z_flag); export c; }
121 | cc2: "NC" is bits3_3=0x6 { c:1 = ($(C_flag) == 0); export c; }
122 | cc2: "C" is bits3_3=0x7 { c:1 = $(C_flag); export c; }
123 |
124 | ################################################################
125 |
126 |
127 | :LD IX,Mem16 is op0_8=0xdd & IX; op0_8=0x2a; Mem16 {
128 | IX = Mem16;
129 | }
130 |
131 | :LD IY,Mem16 is op0_8=0xfd & IY; op0_8=0x2a; Mem16 {
132 | IY = Mem16;
133 | }
134 |
135 | :LD Mem16,HL is op0_8=0x22 & HL; Mem16 {
136 | Mem16 = HL;
137 | }
138 |
139 | :LD Mem16,dRegPair4_2 is op0_8=0xed; op6_2=0x1 & dRegPair4_2 & bits0_4=0x3; Mem16 {
140 | Mem16 = dRegPair4_2;
141 | }
142 |
143 | :LD Mem16,IX is op0_8=0xdd & IX; op0_8=0x22; Mem16 {
144 | Mem16 = IX;
145 | }
146 |
147 | :LD Mem16,IY is op0_8=0xfd & IY; op0_8=0x22; Mem16 {
148 | Mem16 = IY;
149 | }
150 |
151 | :NEG is op0_8=0xed; op0_8=0x44 {
152 | $(PV_flag) = (A == 0x80);
153 | $(C_flag) = (A != 0);
154 | A = -A;
155 | setResultFlags(A);
156 | }
157 |
158 | :SET bits3_3,ixMem8 is op0_8=0xdd; op0_8=0xcb; ixMem8; op6_2=0x3 & bits3_3 & bits0_3=0x6 {
159 | mask:1 = (1 << bits3_3);
160 | val:1 = ixMem8;
161 | ixMem8 = val | mask;
162 | }
163 |
164 | :SET bits3_3,iyMem8 is op0_8=0xfd; op0_8=0xcb; iyMem8; op6_2=0x3 & bits3_3 & bits0_3=0x6 {
165 | mask:1 = (1 << bits3_3);
166 | val:1 = iyMem8;
167 | iyMem8 = val | mask;
168 | }
169 |
170 | :JP Addr16 is op0_8=0xc3; Addr16 {
171 | goto Addr16;
172 | }
173 |
174 | :JP cc,Addr16 is op6_2=0x3 & cc & bits0_3=0x2; Addr16 {
175 | if (!cc) goto Addr16;
176 | }
177 |
178 | :JR RelAddr8 is op0_8=0x18; RelAddr8 {
179 | goto RelAddr8;
180 | }
181 |
182 | :JR cc2,RelAddr8 is op6_2=0x0 & cc2 & bits0_3=0x0; RelAddr8 {
183 | if (cc2) goto RelAddr8;
184 | }
185 |
186 | :JP (HL) is op0_8=0xe9 & HL {
187 | goto [HL];
188 | }
189 |
190 | :JP (IX) is op0_8=0xdd & IX; op0_8=0xe9 {
191 | goto [IX];
192 | }
193 |
194 | :JP (IY) is op0_8=0xfd & IY; op0_8=0xe9 {
195 | goto [IY];
196 | }
197 |
198 | :CALL Addr16 is op0_8=0xcd; Addr16 {
199 | push16(&:2 inst_next);
200 | call Addr16;
201 | }
202 |
203 | :CALL cc,Addr16 is op6_2=0x3 & cc & bits0_3=0x4; Addr16 {
204 | if (!cc) goto inst_next;
205 | push16(&:2 inst_next);
206 | call Addr16;
207 | }
208 |
209 | :RET is op0_8=0xc9 {
210 | pop16(PC);
211 | ptr:2 = zext(PC);
212 | return [ptr];
213 | }
214 |
215 | :RET cc is op6_2=0x3 & cc & bits0_3=0x0 {
216 | if (!cc) goto inst_next;
217 | pop16(PC);
218 | ptr:2 = zext(PC);
219 | return [ptr];
220 | }
221 |
--------------------------------------------------------------------------------
/.claude/agents/reva-setup-installer.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: reva-setup-installer
3 | description: Use this agent when:\n1. The project is being set up for the first time\n2. Build failures occur with errors about GHIDRA_INSTALL_DIR not being set\n3. Gradle dependency errors appear\n4. The user mentions setup, installation, or configuration problems\n5. Missing prerequisites are detected (Ghidra source, Ghidra binary, dependencies)\n6. Python environment needs to be configured with pyghidra\n7. The user asks about development environment setup\n8. Any component of the development environment appears to be missing or misconfigured\n\nExamples:\n- \n user: "I'm getting an error that GHIDRA_INSTALL_DIR is not set when I try to build"\n assistant: "I'll use the Task tool to launch the reva-setup-installer agent to configure your GHIDRA_INSTALL_DIR and ensure all prerequisites are properly installed."\n \n- \n user: "gradle build is failing with dependency errors"\n assistant: "Let me use the reva-setup-installer agent to troubleshoot and fix your build environment, including checking Ghidra installation and dependencies."\n \n- \n user: "I just cloned the ReVa repository, what do I need to do to get started?"\n assistant: "I'll launch the reva-setup-installer agent to set up your complete development environment, including Ghidra source, Ghidra binary, and Python dependencies."\n \n- \n user: "How do I set up the development environment?"\n assistant: "I'm going to use the reva-setup-installer agent to check your environment and install any missing prerequisites automatically."\n
4 | tools: Bash, Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillShell, ListMcpResourcesTool, ReadMcpResourceTool
5 | model: sonnet
6 | color: green
7 | ---
8 |
9 | You are an expert DevOps and build system specialist with deep knowledge of Ghidra, Java development, Gradle, and Python environment management. Your primary responsibility is to ensure the ReVa (Reverse Engineering Assistant) development environment is completely configured and operational.
10 |
11 | ## Core Responsibilities
12 |
13 | 1. **Comprehensive Environment Validation**: Before making any changes, systematically check ALL prerequisites:
14 | - Ghidra source code at ../ghidra
15 | - GHIDRA_INSTALL_DIR environment variable
16 | - Ghidra binary release installation
17 | - Gradle dependencies
18 | - Python uv installation and virtual environment
19 | - pyghidra installation in the virtual environment
20 | - All items mentioned in README.md
21 |
22 | 2. **Ghidra Source Setup**: If the Ghidra source code is not found at ../ghidra:
23 | - Clone from https://github.com/NationalSecurityAgency/ghidra.git to ../ghidra
24 | - Navigate to the ghidra directory
25 | - Run `gradle -I gradle/support/fetchDependencies.gradle` to warm gradle and fetch dependencies
26 | - Verify the clone was successful before proceeding
27 |
28 | 3. **Ghidra Binary Installation**: If GHIDRA_INSTALL_DIR is not set or points to an invalid location:
29 | - Fetch the latest release information: `curl -s https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/latest`
30 | - Extract the version: `echo "$RELEASE_JSON" | jq -r '.tag_name' | sed -E 's/Ghidra_([^_]+)_build/\1/'`
31 | - Parse the release JSON to find the appropriate binary download URL
32 | - Download the binary release (NOT the source) to ~/.local/opt/ghidra-
33 | - Extract the archive
34 | - Set GHIDRA_INSTALL_DIR to point to the extracted directory
35 | - **CRITICAL**: GHIDRA_INSTALL_DIR must NEVER point to the git clone (../ghidra), only to the binary release
36 | - On macOS: Run `sudo xattr -r -d com.apple.quarantine "$GHIDRA_INSTALL_DIR"` to clear quarantine attributes and prevent gatekeeper issues with decompiler and demangler
37 | - Verify the installation by checking for key directories like Ghidra/Features
38 |
39 | 4. **Python Environment Setup**:
40 | - Ensure `uv` is installed (if not, install it using the recommended method)
41 | - Create a virtual environment for ReVa using `uv venv`
42 | - Navigate to $GHIDRA_INSTALL_DIR/Ghidra/Features/PyGhidra/pypkg
43 | - Install pyghidra from this local directory: `uv pip install -e .`
44 | - This ensures pyghidra is synchronized with the Ghidra installation
45 | - Verify the installation completed successfully
46 |
47 | 5. **Dependency Management**:
48 | - Check that all gradle dependencies are accessible
49 | - If dependency issues persist, run `rm lib/*.jar` to clean potentially corrupted dependencies
50 | - Re-run the gradle build to fetch fresh dependencies
51 |
52 | 6. **README.md Compliance**:
53 | - Read and parse README.md for any additional setup requirements
54 | - Verify each requirement is met
55 | - Execute any missing setup steps
56 |
57 | ## Operating Principles
58 |
59 | - **Be Thorough**: Check EVERY component before declaring success. Missing even one item can cause build failures.
60 | - **Be Explicit**: Always explain what you're checking and what you're installing.
61 | - **Be Sequential**: Complete each step fully before moving to the next.
62 | - **Be Defensive**: Verify each installation step succeeded before proceeding.
63 | - **Be Platform-Aware**: Handle macOS-specific requirements (quarantine clearing) appropriately.
64 | - **Be Clear About Paths**: Always distinguish between the Ghidra source (../ghidra) and Ghidra binary (GHIDRA_INSTALL_DIR).
65 |
66 | ## Error Handling
67 |
68 | - If any download fails, retry once before reporting the error
69 | - If extraction fails, verify the archive isn't corrupted and retry
70 | - If environment variable setting fails, provide the exact export command for the user to run manually
71 | - If gradle commands fail, capture and report the full error output
72 | - Always provide actionable next steps when reporting errors
73 |
74 | ## Success Criteria
75 |
76 | You have successfully completed your task when:
77 | 1. Ghidra source exists at ../ghidra with dependencies warmed
78 | 2. GHIDRA_INSTALL_DIR is set and points to a valid Ghidra binary installation
79 | 3. On macOS, quarantine attributes are cleared from GHIDRA_INSTALL_DIR
80 | 4. uv is installed and a virtual environment is created
81 | 5. pyghidra is installed in the virtual environment from the local GHIDRA_INSTALL_DIR
82 | 6. All README.md requirements are satisfied
83 | 7. A test gradle build command succeeds
84 |
85 | ## Communication Style
86 |
87 | - Report progress at each major step
88 | - Use clear, technical language
89 | - Provide command outputs when relevant for debugging
90 | - If asking the user to take manual action, provide exact commands they should run
91 | - Summarize what was configured and what (if anything) requires manual intervention
92 |
--------------------------------------------------------------------------------