diff --git a/capa/ghidra/plugin/extension/README.md b/capa/ghidra/plugin/extension/README.md new file mode 100644 index 000000000..08fe9847f --- /dev/null +++ b/capa/ghidra/plugin/extension/README.md @@ -0,0 +1 @@ +# CapaExplorer diff --git a/capa/ghidra/plugin/extension/build.gradle b/capa/ghidra/plugin/extension/build.gradle new file mode 100644 index 000000000..a3a305cb9 --- /dev/null +++ b/capa/ghidra/plugin/extension/build.gradle @@ -0,0 +1,66 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Builds a Ghidra Extension for a given Ghidra installation. +// +// An absolute path to the Ghidra installation directory must be supplied either by setting the +// GHIDRA_INSTALL_DIR environment variable or Gradle project property: +// +// > export GHIDRA_INSTALL_DIR= +// > gradle +// +// or +// +// > gradle -PGHIDRA_INSTALL_DIR= +// +// Gradle should be invoked from the directory of the project to build. Please see the +// application.gradle.version property in /Ghidra/application.properties +// for the correction version of Gradle to use for the Ghidra installation you specify. + +//----------------------START "DO NOT MODIFY" SECTION------------------------------ +def ghidraInstallDir + +if (System.env.GHIDRA_INSTALL_DIR) { + ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR +} +else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { + ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") +} +else { + ghidraInstallDir = "" +} + +task distributeExtension { + group = "Ghidra" + + apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" + dependsOn ':buildExtension' +} +//----------------------END "DO NOT MODIFY" SECTION------------------------------- + +repositories { + // Declare dependency repositories here. This is not needed if dependencies are manually + // dropped into the lib/ directory. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html for more info. + // Ex: mavenCentral() +} + +dependencies { + // Any external dependencies added here will automatically be copied to the lib/ directory when + // this extension is built. +} + +// Exclude additional files from the built extension +// Ex: buildExtension.exclude '.idea/**' diff --git a/capa/ghidra/plugin/extension/extension.properties b/capa/ghidra/plugin/extension/extension.properties new file mode 100644 index 000000000..ee393d8a3 --- /dev/null +++ b/capa/ghidra/plugin/extension/extension.properties @@ -0,0 +1,5 @@ +name=@extname@ +description=Finds capabilities in programs, as detected by capa. +author=Mandiant +createdOn=2026-01-26 +version=@extversion@ \ No newline at end of file diff --git a/capa/ghidra/plugin/extension/src/main/help/help/TOC_Source.xml b/capa/ghidra/plugin/extension/src/main/help/help/TOC_Source.xml new file mode 100644 index 000000000..a34f62e8f --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/help/help/TOC_Source.xml @@ -0,0 +1,57 @@ + + + + + + + diff --git a/capa/ghidra/plugin/extension/src/main/help/help/topics/capaexplorer/help.html b/capa/ghidra/plugin/extension/src/main/help/help/topics/capaexplorer/help.html new file mode 100644 index 000000000..1f9d6a1fc --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/help/help/topics/capaexplorer/help.html @@ -0,0 +1,23 @@ + + + + + + + + + + + Skeleton Help File for a Module + + + + +

Skeleton Help File for a Module

+ +

This is a simple skeleton help topic. For a better description of what should and should not + go in here, see the "sample" Ghidra extension in the Extensions/Ghidra directory, or see your + favorite help topic. In general, language modules do not have their own help topics.

+ + diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPlugin.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPlugin.java new file mode 100644 index 000000000..6ead8b1d9 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPlugin.java @@ -0,0 +1,51 @@ +package capa.ghidra; + +import docking.ActionContext; +import docking.action.DockingAction; +import docking.action.MenuData; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; + +@PluginInfo( + status = PluginStatus.STABLE, + packageName = "Capa", + category = PluginCategoryNames.ANALYSIS, + shortDescription = "Run capa analysis", + description = "Capa explorer MVP for Ghidra" +) +public class CapaPlugin extends ProgramPlugin { + + private CapaProvider provider; + + public CapaPlugin(PluginTool tool) { + super(tool); + + provider = new CapaProvider(tool); + createActions(); + } + + private void createActions() { + + DockingAction action = + new DockingAction("Run capa analysis", getName()) { + + @Override + public void actionPerformed(ActionContext context) { + provider.runCapa(currentProgram); + } + }; + + action.setMenuBarData( + new MenuData(new String[] { + "Tools", + "Run capa analysis" + }) + ); + + tool.addAction(action); + } +} + diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaProvider.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaProvider.java new file mode 100644 index 000000000..14dff43ec --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaProvider.java @@ -0,0 +1,66 @@ +package capa.ghidra; + +import docking.ComponentProvider; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JComponent; +import java.awt.Font; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class CapaProvider extends ComponentProvider { + + private final PluginTool tool; + private final JTextArea output; + + public CapaProvider(PluginTool tool) { + super(tool, "Capa Explorer", "CapaExplorer"); + this.tool = tool; + + output = new JTextArea(); + output.setEditable(false); + output.setFont(new Font("Monospaced", Font.PLAIN, 12)); + + tool.addComponentProvider(this, true); + tool.showComponentProvider(this, true); + } + + @Override + public JComponent getComponent() { + return new JScrollPane(output); + } + + public void runCapa(Program program) { + + // Ensure panel is visible + tool.showComponentProvider(this, true); + + if (program == null) { + output.setText("No program is currently open.\n"); + return; + } + + output.setText("Running capa...\n"); + + try { + String result = CapaPythonBridge.run(program); + output.append(result); + } + catch (Exception e) { + Msg.error(this, "Error running capa", e); + + output.append("\nERROR:\n"); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + + output.append(sw.toString()); + } + } +} diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPythonBridge.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPythonBridge.java new file mode 100644 index 000000000..0ebc9b255 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPythonBridge.java @@ -0,0 +1,72 @@ +package capa.ghidra; + +import generic.jar.ResourceFile; +import ghidra.framework.Application; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; + +public class CapaPythonBridge { + /** + * Execute capa analysis for the currently loaded program. + * + * NOTE: The Program parameter is intentionally unused in this MVP. + * In future iterations, program metadata (path, architecture, hashes, + * memory layout, etc.) will be passed to the Python capa runner. + */ + + public static String run(Program program) throws Exception { + + ResourceFile pythonDir = + Application.getModuleDataSubDirectory("python"); + + File scriptFile = + new File( + pythonDir.getFile(false), + "capa_runner.py" + ); + + String python = System.getenv("CAPA_PYTHON"); + if (python == null || python.isBlank()) { + python = "python3"; + } + + Msg.info( + CapaPythonBridge.class, + "Using Python executable: " + python + ); + + Process process = + new ProcessBuilder( + python, + scriptFile.getAbsolutePath() + ) + .redirectErrorStream(true) + .start(); + + StringBuilder output = new StringBuilder(); + + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader( + process.getInputStream()))) { + + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append('\n'); + } + } + + int exit = process.waitFor(); + if (exit != 0) { + throw new RuntimeException( + "Python exited with code " + exit + ); + } + + return output.toString(); + } +} diff --git a/capa/ghidra/plugin/extension/src/main/resources/images/README.txt b/capa/ghidra/plugin/extension/src/main/resources/images/README.txt new file mode 100644 index 000000000..f20ae77b7 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/resources/images/README.txt @@ -0,0 +1,2 @@ +The "src/resources/images" directory is intended to hold all image/icon files used by +this module.