diff --git a/.gitignore b/.gitignore
index cef85dbae6..11fee7e039 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,3 +129,10 @@ uv.lock
.github/binja/BinaryNinja-headless.zip
justfile
data/
+.gradle.gradle/
+build/
+.DS_Store
+.gradle/
+**/.gradle/
+.DS_Store
+**/.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd49da66c9..5b0cb784ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### New Features
+- ghidra: experimental capa explorer extension (MVP) @vaishakh787
- ghidra: support PyGhidra @mike-hunhoff #2788
- vmray: extract number features from whitelisted void_ptr parameters (hKey, hKeyRoot) @adeboyedn #2835
diff --git a/capa/ghidra/plugin/extension/README.md b/capa/ghidra/plugin/extension/README.md
new file mode 100644
index 0000000000..08fe9847ff
--- /dev/null
+++ b/capa/ghidra/plugin/extension/README.md
@@ -0,0 +1 @@
+# CapaExplorer
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/AbstractTreeTableModel.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/AbstractTreeTableModel.class
new file mode 100644
index 0000000000..2c8c9978f0
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/AbstractTreeTableModel.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaCacheManager.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaCacheManager.class
new file mode 100644
index 0000000000..b471ee310d
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaCacheManager.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData$NodeType.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData$NodeType.class
new file mode 100644
index 0000000000..b09056ea32
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData$NodeType.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData.class
new file mode 100644
index 0000000000..f9c2421e50
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaNodeData.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask$1.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask$1.class
new file mode 100644
index 0000000000..d53945b827
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask$1.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask.class
new file mode 100644
index 0000000000..3432485511
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin$CapaAnalysisTask.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin.class
new file mode 100644
index 0000000000..fd50585cc6
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaPlugin.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$1.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$1.class
new file mode 100644
index 0000000000..d8bd728b87
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$1.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$2.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$2.class
new file mode 100644
index 0000000000..27bffeaa16
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider$2.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider.class
new file mode 100644
index 0000000000..864e66d648
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaProvider.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability$Match.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability$Match.class
new file mode 100644
index 0000000000..4bc96441b5
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability$Match.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability.class
new file mode 100644
index 0000000000..c91ae90ffb
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$Capability.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock$Permissions.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock$Permissions.class
new file mode 100644
index 0000000000..131f9c6fae
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock$Permissions.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock.class
new file mode 100644
index 0000000000..19d52ec867
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults$MemoryBlock.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults.class
new file mode 100644
index 0000000000..6817aa8da0
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResults.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResultsTreeModel.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResultsTreeModel.class
new file mode 100644
index 0000000000..077b88fdae
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/CapaResultsTreeModel.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$ListToTreeSelectionModelWrapper.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$ListToTreeSelectionModelWrapper.class
new file mode 100644
index 0000000000..2d21d33e6d
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$ListToTreeSelectionModelWrapper.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellEditor.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellEditor.class
new file mode 100644
index 0000000000..1189770e29
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellEditor.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellRenderer.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellRenderer.class
new file mode 100644
index 0000000000..631c6fcb5b
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable$TreeTableCellRenderer.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable.class
new file mode 100644
index 0000000000..76d5eeeed5
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/JTreeTable.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel$TreeTableModelMarker.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel$TreeTableModelMarker.class
new file mode 100644
index 0000000000..6671edd62a
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel$TreeTableModelMarker.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel.class
new file mode 100644
index 0000000000..de6064e436
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModel.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$1.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$1.class
new file mode 100644
index 0000000000..985784aa2e
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$1.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$2.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$2.class
new file mode 100644
index 0000000000..e209748144
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter$2.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter.class b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter.class
new file mode 100644
index 0000000000..c27e4af3b7
Binary files /dev/null and b/capa/ghidra/plugin/extension/bin/main/capa/ghidra/TreeTableModelAdapter.class differ
diff --git a/capa/ghidra/plugin/extension/bin/main/help/TOC_Source.xml b/capa/ghidra/plugin/extension/bin/main/help/TOC_Source.xml
new file mode 100644
index 0000000000..a34f62e8f1
--- /dev/null
+++ b/capa/ghidra/plugin/extension/bin/main/help/TOC_Source.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
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/bin/main/images/README.txt b/capa/ghidra/plugin/extension/bin/main/images/README.txt new file mode 100644 index 0000000000..f20ae77b73 --- /dev/null +++ b/capa/ghidra/plugin/extension/bin/main/images/README.txt @@ -0,0 +1,2 @@ +The "src/resources/images" directory is intended to hold all image/icon files used by +this module. diff --git a/capa/ghidra/plugin/extension/bin/scripts/README.txt b/capa/ghidra/plugin/extension/bin/scripts/README.txt new file mode 100644 index 0000000000..9e408f4be9 --- /dev/null +++ b/capa/ghidra/plugin/extension/bin/scripts/README.txt @@ -0,0 +1 @@ +Java source directory to hold module-specific Ghidra scripts. diff --git a/capa/ghidra/plugin/extension/bin/scripts/RunCapaMVP.py b/capa/ghidra/plugin/extension/bin/scripts/RunCapaMVP.py new file mode 100644 index 0000000000..e6ec0e03eb --- /dev/null +++ b/capa/ghidra/plugin/extension/bin/scripts/RunCapaMVP.py @@ -0,0 +1,93 @@ +import json +import logging +import os +import pathlib +import sys +import traceback + +logger = logging.getLogger("capa_explorer") + +def find_config(): + home = os.path.expanduser("~") + candidates = [ + os.path.join(home, "Library", "ghidra", "ghidra_12.0.3_PUBLIC", "capa_cache", "config.json"), + os.path.join(home, ".ghidra", ".ghidra_12.0.3_PUBLIC", "capa_cache", "config.json"), + ] + env_dir = os.environ.get("GHIDRA_USER_SETTINGS_DIR") + if env_dir: + candidates.insert(0, os.path.join(env_dir, "capa_cache", "config.json")) + + for path in candidates: + if os.path.isfile(path): + with open(path, "r") as f: + return json.load(f) + raise RuntimeError("capa config.json not found") + +def main(): + logging.basicConfig(level=logging.INFO) + print("[RunCapaMVP] Starting capa analysis via PyGhidra...") + + # Initialize GhidraContext with the correct 3 arguments + from capa.features.extractors.ghidra import context as ghidra_ctx_module + from ghidra.program.flatapi import FlatProgramAPI # pylint: disable=import-error + + flat_api = FlatProgramAPI(currentProgram, monitor) + + # Set the module-level singleton using set_context() + ghidra_ctx_module.set_context(currentProgram, flat_api, monitor) + + # Load config + config = find_config() + rules_dir = config.get("rulesDirectory") or config.get("rules_directory") + output_path = config.get("outputPath") + + print("[RunCapaMVP] Rules :", rules_dir) + print("[RunCapaMVP] Output:", output_path) + + # Import Capa modules + import capa.rules + import capa.rules.cache + import capa.ghidra.helpers + import capa.capabilities.common + import capa.render.json as capa_render_json + import capa.features.extractors.ghidra.extractor + + # Run checks + if not capa.ghidra.helpers.is_supported_ghidra_version(): + raise RuntimeError("Unsupported Ghidra version") + if not capa.ghidra.helpers.is_supported_file_type(): + raise RuntimeError("Unsupported file type") + if not capa.ghidra.helpers.is_supported_arch_type(): + raise RuntimeError("Unsupported architecture") + + # Load rules and run analysis + rules_path = pathlib.Path(rules_dir) + print("[RunCapaMVP] Loading rules...") + rules = capa.rules.get_rules([rules_path], cache_dir=None) + meta = capa.ghidra.helpers.collect_metadata([rules_path]) + + print("[RunCapaMVP] Creating GhidraFeatureExtractor...") + extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() + + print("[RunCapaMVP] Running capability detection...") + capabilities = capa.capabilities.common.find_capabilities(rules, extractor, False) + + meta.analysis.feature_counts = capabilities.feature_counts + meta.analysis.library_functions = capabilities.library_functions + + print("[RunCapaMVP] Rendering results...") + result_json = capa_render_json.render(meta, rules, capabilities.matches) + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w") as f: + f.write(result_json) + + print("[RunCapaMVP] Done — results written to:", output_path) + +try: + main() +except Exception as e: + # Safe error printing + sys.stderr.write(f"[RunCapaMVP] FATAL: {e}\n") + traceback.print_exc() + raise diff --git a/capa/ghidra/plugin/extension/bin/scripts/capa_explorer.py b/capa/ghidra/plugin/extension/bin/scripts/capa_explorer.py new file mode 100644 index 0000000000..cc3ef6f9a9 --- /dev/null +++ b/capa/ghidra/plugin/extension/bin/scripts/capa_explorer.py @@ -0,0 +1,10 @@ +#@author capa +#@category Analysis +#@menupath Tools.Run capa analysis + +program = currentProgram + +print("PyGhidra OK") +print("Program:", program.getName()) +print("Functions:", + program.getFunctionManager().getFunctionCount()) \ No newline at end of file diff --git a/capa/ghidra/plugin/extension/build.gradle b/capa/ghidra/plugin/extension/build.gradle new file mode 100644 index 0000000000..f5d2daf992 --- /dev/null +++ b/capa/ghidra/plugin/extension/build.gradle @@ -0,0 +1,56 @@ +/* ### + * 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. + */ + +//----------------------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 = "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/AbstractTreeTableModel.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/AbstractTreeTableModel.java new file mode 100644 index 0000000000..d56c9ecda2 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/AbstractTreeTableModel.java @@ -0,0 +1,95 @@ +package capa.ghidra; + +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreePath; + +/** + * Abstract base class for TreeTableModel implementations. + * Handles listener management and event firing. + */ +public abstract class AbstractTreeTableModel implements TreeTableModel { + + protected Object root; + protected EventListenerList listenerList = new EventListenerList(); + + public AbstractTreeTableModel(Object root) { + this.root = root; + } + + // ------------------------------------------------------------------------- + // TreeModel + // ------------------------------------------------------------------------- + + @Override + public Object getRoot() { + return root; + } + + @Override + public boolean isLeaf(Object node) { + return getChildCount(node) == 0; + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + // not used + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + int count = getChildCount(parent); + for (int i = 0; i < count; i++) { + if (getChild(parent, i).equals(child)) { + return i; + } + } + return -1; + } + + @Override + public void addTreeModelListener(TreeModelListener l) { + listenerList.add(TreeModelListener.class, l); + } + + @Override + public void removeTreeModelListener(TreeModelListener l) { + listenerList.remove(TreeModelListener.class, l); + } + + // ------------------------------------------------------------------------- + // TreeTableModel defaults + // ------------------------------------------------------------------------- + + @Override + public boolean isCellEditable(Object node, int column) { + // Only the tree column (0) is "editable" so the tree can receive events + return getColumnClass(column) == TreeTableModel.class; + } + + @Override + public void setValueAt(Object aValue, Object node, int column) { + // not editable + } + + // ------------------------------------------------------------------------- + // Event helpers + // ------------------------------------------------------------------------- + + protected void fireTreeStructureChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); + for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { + l.treeStructureChanged(event); + } + } + + protected void fireTreeNodesChanged(Object source, Object[] path, + int[] childIndices, Object[] children) { + TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children); + for (TreeModelListener l : listenerList.getListeners(TreeModelListener.class)) { + l.treeNodesChanged(event); + } + } +} \ No newline at end of file diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaCacheManager.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaCacheManager.java new file mode 100644 index 0000000000..73954ac020 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaCacheManager.java @@ -0,0 +1,176 @@ +package capa.ghidra; + +import com.google.gson.*; +import ghidra.framework.Application; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +public class CapaCacheManager { + + private static final long MAX_CACHE_SIZE = 100L * 1024 * 1024; // 100 MB + private static final String CACHE_DIR_NAME = "capa_cache"; + private static final String CONFIG_FILE = "config.json"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + // Cache directory + + private static Path getCacheDir() throws IOException { + File userDir = Application.getUserSettingsDirectory(); + Path cacheDir = userDir.toPath().resolve(CACHE_DIR_NAME); + if (!Files.exists(cacheDir)) { + Files.createDirectories(cacheDir); + setPosixPerms(cacheDir, "rwx------"); + } + return cacheDir; + } + + // Per-binary result cache + + public static String computeProgramHash(Program program) { + try { + String id = program.getName() + "|" + + (program.getExecutablePath() != null ? program.getExecutablePath() : ""); + byte[] hash = MessageDigest.getInstance("SHA-256") + .digest(id.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } catch (NoSuchAlgorithmException e) { + return String.valueOf(Math.abs(program.getName().hashCode())); + } + } + + private static Path getCacheFilePath(String hash) throws IOException { + if (!hash.matches("^[0-9a-f]+$")) throw new SecurityException("Invalid hash"); + Path dir = getCacheDir(); + Path file = dir.resolve(hash + ".json"); + if (!file.normalize().startsWith(dir.normalize())) + throw new SecurityException("Path traversal detected"); + return file; + } + + public static boolean cacheExists(Program program) { + try { + Path f = getCacheFilePath(computeProgramHash(program)); + return Files.exists(f) && Files.size(f) <= MAX_CACHE_SIZE; + } catch (Exception e) { + return false; + } + } + + public static String readCache(Program program) { + try { + Path f = getCacheFilePath(computeProgramHash(program)); + if (!Files.exists(f) || Files.size(f) > MAX_CACHE_SIZE) return null; + return Files.readString(f, StandardCharsets.UTF_8); + } catch (Exception e) { + Msg.error(CapaCacheManager.class, "readCache failed", e); + return null; + } + } + + public static boolean writeCache(Program program, String json) { + try { + Path file = getCacheFilePath(computeProgramHash(program)); + Path tmp = file.resolveSibling(file.getFileName() + ".tmp"); + Files.writeString(tmp, json, StandardCharsets.UTF_8); + setPosixPerms(tmp, "rw-------"); + Files.move(tmp, file, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + return true; + } catch (Exception e) { + Msg.error(CapaCacheManager.class, "writeCache failed", e); + return false; + } + } + + public static boolean deleteCache(Program program) { + try { + Path f = getCacheFilePath(computeProgramHash(program)); + return Files.deleteIfExists(f); + } catch (Exception e) { + Msg.error(CapaCacheManager.class, "deleteCache failed", e); + return false; + } + } + + + public static String getCacheFilePathForPython(Program program) { + try { + return getCacheFilePath(computeProgramHash(program)).toAbsolutePath().toString(); + } catch (Exception e) { + Msg.error(CapaCacheManager.class, "getCacheFilePathForPython failed", e); + return null; + } + } + + // Config — rules directory + private static Path getConfigFilePath() throws IOException { + return getCacheDir().resolve(CONFIG_FILE); + } + + private static JsonObject readConfig() { + try { + Path cfg = getConfigFilePath(); + if (!Files.exists(cfg)) return new JsonObject(); + String raw = Files.readString(cfg, StandardCharsets.UTF_8); + return JsonParser.parseString(raw).getAsJsonObject(); + } catch (Exception e) { + return new JsonObject(); + } + } + + private static void writeConfig(JsonObject config) { + try { + Path cfg = getConfigFilePath(); + Files.writeString(cfg, GSON.toJson(config), StandardCharsets.UTF_8); + } catch (Exception e) { + Msg.error(CapaCacheManager.class, "writeConfig failed", e); + } + } + + public static String readRulesDirectory() { + JsonObject cfg = readConfig(); + // Support both key names for backwards compatibility + for (String key : new String[]{"rulesDirectory", "rules_directory"}) { + if (cfg.has(key) && !cfg.get(key).isJsonNull()) { + String val = cfg.get(key).getAsString().trim(); + if (!val.isEmpty()) return val; + } + } + return null; + } + + public static void writeRulesDirectory(String path) { + JsonObject cfg = readConfig(); + cfg.addProperty("rulesDirectory", path); + // Keep legacy key for any older code + cfg.addProperty("rules_directory", path); + writeConfig(cfg); + } + + public static void writeAnalysisConfig(String rulesDir, String outputPath) { + JsonObject cfg = readConfig(); + cfg.addProperty("rulesDirectory", rulesDir); + cfg.addProperty("rules_directory", rulesDir); + cfg.addProperty("outputPath", outputPath); + writeConfig(cfg); + } + + // Helpers + + private static void setPosixPerms(Path path, String perms) { + try { + Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(perms)); + } catch (UnsupportedOperationException | IOException ignored) { + // Windows — no-op + } + } +} \ No newline at end of file diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaNodeData.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaNodeData.java new file mode 100644 index 0000000000..148a71a4f3 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaNodeData.java @@ -0,0 +1,44 @@ +package capa.ghidra; + +/** + * CapaNodeData holds the three column values for a single row in the capa JTreeTable. + * + * Column 0 – Rule Information (shown in the tree column) + * Column 1 – Address (hex address or empty) + * Column 2 – Details (namespace / call detail / operand) + */ +public class CapaNodeData { + + public enum NodeType { + RULE, // top-level rule row e.g. "create HTTP request (2 matches)" + FUNCTION, // function-scope match "function(sub_401880)" + BASIC_BLOCK, // basic block scope "basic block @ 0x…" + STATEMENT, // and / or / optional / not + FEATURE // leaf feature api(...) / string(...) / number(...) / regex(...) + } + + private final String label; // displayed in col-0 tree + private final String address; // col-1 + private final String details; // col-2 + private final NodeType type; + + public CapaNodeData(String label, String address, String details, NodeType type) { + this.label = label != null ? label : ""; + this.address = address != null ? address : ""; + this.details = details != null ? details : ""; + this.type = type; + } + + /** Convenience constructor when type is not critical (statements/features). */ + public CapaNodeData(String label, String address, String details) { + this(label, address, details, NodeType.FEATURE); + } + + public String getLabel() { return label; } + public String getAddress() { return address; } + public String getDetails() { return details; } + public NodeType getNodeType() { return type; } + + @Override + public String toString() { return label; } +} \ No newline at end of file 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 0000000000..f31a4cfd82 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaPlugin.java @@ -0,0 +1,226 @@ +package capa.ghidra; + +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.script.GhidraScriptUtil; +import ghidra.app.services.GhidraScriptService; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; +import ghidra.util.task.Task; +import ghidra.util.task.TaskLauncher; +import ghidra.util.task.TaskListener; +import ghidra.util.task.TaskMonitor; +import generic.jar.ResourceFile; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +@PluginInfo( + status = PluginStatus.STABLE, + packageName = "Capa", + category = PluginCategoryNames.ANALYSIS, + shortDescription = "capa capability detection", + description = "Integrates Mandiant capa malware capability detection with Ghidra. " + + "All actions are performed from the Capa Explorer window." +) +public class CapaPlugin extends ProgramPlugin { + + private CapaProvider provider; + + // Lifecycle + + public CapaPlugin(PluginTool tool) { + super(tool); + } + + @Override + protected void init() { + super.init(); + provider = new CapaProvider(tool, getName(), this); + } + + @Override + protected void programActivated(Program program) { + provider.onProgramActivated(program); + } + + @Override + protected void programDeactivated(Program program) { + provider.onProgramDeactivated(program); + } + + // Analysis entry-point + + public void runAnalysis(boolean forceRerun) { + Program program = currentProgram; + if (program == null) { + Msg.showInfo(this, null, "Capa Analysis", "No program is currently open."); + return; + } + + String rulesDir = CapaCacheManager.readRulesDirectory(); + if (rulesDir == null || rulesDir.isEmpty()) { + Msg.showInfo(this, null, "Capa Analysis", + "No rules directory configured.\n\nClick Settings and select your capa-rules folder."); + return; + } + + tool.showComponentProvider(provider, true); + + // Load from cache if available and not forcing rerun + if (!forceRerun && CapaCacheManager.cacheExists(program)) { + String json = CapaCacheManager.readCache(program); + if (json != null) { + provider.displayResults(json); + return; + } + } + + TaskLauncher.launch(new CapaAnalysisTask(program, rulesDir)); + } + + // Background analysis task + + + private class CapaAnalysisTask extends Task { + + private final Program program; + private final String rulesDir; + + CapaAnalysisTask(Program program, String rulesDir) { + super("Running capa analysis", true, false, true); + this.program = program; + this.rulesDir = rulesDir; + } + + @Override + public void run(TaskMonitor monitor) { + provider.showLoading("Running capa analysis\u2026"); + + try { + // Determine the cache file path Python will write to + String outputPath = CapaCacheManager.getCacheFilePathForPython(program); + if (outputPath == null) { + showError("Could not determine cache file path."); + return; + } + + // Delete stale cache so we can reliably detect new output + Files.deleteIfExists(Path.of(outputPath)); + + // Write both rulesDir and outputPath into config.json + // so RunCapaMVP.py can read them without recomputing the hash + CapaCacheManager.writeAnalysisConfig(rulesDir, outputPath); + + // Locate RunCapaMVP.py bundled with this extension + String scriptPath = resolveScriptPath(); + if (scriptPath == null) { + showError("Could not locate RunCapaMVP.py in the extension's ghidra_scripts folder."); + return; + } + + // Invoke via GhidraScriptService — executes inside the PyGhidra environment. + // The script receives the current Program context automatically. + monitor.setMessage("Invoking capa via PyGhidra\u2026"); + GhidraScriptService scriptService = tool.getService(GhidraScriptService.class); + if (scriptService == null) { + showError("GhidraScriptService not available. Is PyGhidra installed?"); + return; + } + + // GhidraScriptService requires the script to be in a registered + // script directory. Register our ghidra_scripts folder so it + // can find RunCapaMVP.py by name. + File scriptFile = new File(scriptPath); + ResourceFile scriptDir = new ResourceFile(scriptFile.getParentFile()); + + // Add to known script directories if not already present + if (!GhidraScriptUtil.getScriptSourceDirectories().contains(scriptDir)) { + GhidraScriptUtil.getScriptSourceDirectories().add(scriptDir); + } + + scriptService.runScript("RunCapaMVP.py", new TaskListener() { + @Override + public void taskCompleted(Task task) { + Msg.info(CapaPlugin.this, "[capa] Script completed."); + } + @Override + public void taskCancelled(Task task) { + Msg.warn(CapaPlugin.this, "[capa] Script was cancelled."); + } + }); + + // Poll for the output file written by Python + monitor.setMessage("Waiting for capa results\u2026"); + String json = waitForCacheFile(outputPath, monitor); + if (json == null) { + showError("capa did not produce output. Check the Ghidra console for details."); + return; + } + + // Persist to named cache and display in UI + CapaCacheManager.writeCache(program, json); + provider.displayResults(json); + + } catch (Exception e) { + showError("Unexpected error: " + e.getMessage()); + Msg.error(CapaPlugin.this, "capa analysis error", e); + } + } + + private String waitForCacheFile(String outputPath, TaskMonitor monitor) + throws InterruptedException { + final int MAX_WAIT_MS = 300_000; // 5 min — capa can be slow on large binaries + final int POLL_INTERVAL = 500; + long start = System.currentTimeMillis(); + + while (!monitor.isCancelled()) { + Path p = Path.of(outputPath); + if (Files.exists(p)) { + try { + String raw = Files.readString(p, StandardCharsets.UTF_8).trim(); + // Strip any non-JSON preamble before the opening brace + int jsonStart = raw.indexOf('{'); + if (jsonStart > 0) raw = raw.substring(jsonStart); + if (!raw.isEmpty()) return raw; + } catch (Exception ignored) {} + } + if (System.currentTimeMillis() - start > MAX_WAIT_MS) return null; + Thread.sleep(POLL_INTERVAL); + } + return null; + } + + private String resolveScriptPath() { + try { + File jar = new File( + getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + // Extension layout: CapaExplorer/lib/CapaExplorer.jar + // CapaExplorer/ghidra_scripts/RunCapaMVP.py + File extensionDir = jar.getParentFile().getParentFile(); + File[] candidates = { + new File(extensionDir, "ghidra_scripts/RunCapaMVP.py"), + new File(jar.getParentFile(), "ghidra_scripts/RunCapaMVP.py"), + new File(extensionDir, "ghidra_Scripts/RunCapaMVP.py"), + }; + for (File f : candidates) { + Msg.info(CapaPlugin.this, "Checking script path: " + f.getAbsolutePath()); + if (f.exists()) return f.getAbsolutePath(); + } + } catch (Exception e) { + Msg.warn(CapaPlugin.this, "Could not resolve script path: " + e.getMessage()); + } + return null; + } + + private void showError(String msg) { + provider.showError(msg); + Msg.showError(CapaPlugin.this, null, "Capa Analysis Error", msg); + } + } +} \ No newline at end of file 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 0000000000..00a101fedd --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaProvider.java @@ -0,0 +1,414 @@ +package capa.ghidra; + +import com.google.gson.*; +import docking.ComponentProvider; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import ghidra.app.services.GoToService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; + +/** + * CapaProvider — the main Ghidra component window for the capa extension. + * + * UI mirrors the IDA FLARE capa explorer: + * ┌──────────────────────────────────────────────────────┐ + * │ [✓] Limit results to current function [✓] Show matches by function │ + * │ search... │ + * ├──────────────────────────────────────────────────────┤ + * │ Rule Information │ Address │ Details │ + * │ ▶ create HTTP request… │ │ comm/http/… │ + * │ ▼ function(sub_401880) │ 00401880│ │ + * │ and │ │ │ + * │ api(shell32.…) │ 00401981│ call ds:… │ + * ├──────────────────────────────────────────────────────┤ + * │ [Analyze] [Reset] [Settings] [Save] │ + * │ capa rules directory: /path/to/rules (N rules) │ + * └──────────────────────────────────────────────────────┘ + */ +public class CapaProvider extends ComponentProvider { + + // ------------------------------------------------------------------ // + // State // + // ------------------------------------------------------------------ // + + private final CapaPlugin plugin; + private Program currentProgram; + + // UI references + private JPanel mainPanel; + private JCheckBox limitToFunctionCheckbox; + private JCheckBox showByFunctionCheckbox; + private JTextField searchField; + private JTreeTable treeTable; + private CapaResultsTreeModel treeTableModel; + private JScrollPane scrollPane; + + private JButton analyzeButton; + private JButton resetButton; + private JButton settingsButton; + + private JLabel statusLabel; + private JLabel rulesPathLabel; + + // ------------------------------------------------------------------ // + // Constructor // + // ------------------------------------------------------------------ // + + public CapaProvider(PluginTool tool, String owner, CapaPlugin plugin) { + super(tool, "Capa Explorer", owner); + this.plugin = plugin; + buildUI(); + setVisible(true); + } + + // ------------------------------------------------------------------ // + // ComponentProvider API // + // ------------------------------------------------------------------ // + + @Override + public JComponent getComponent() { + return mainPanel; + } + + // ------------------------------------------------------------------ // + // UI construction // + // ------------------------------------------------------------------ // + + private void buildUI() { + mainPanel = new JPanel(new BorderLayout(0, 0)); + mainPanel.setPreferredSize(new Dimension(860, 500)); + + mainPanel.add(buildTopPanel(), BorderLayout.NORTH); + mainPanel.add(buildTreePanel(), BorderLayout.CENTER); + mainPanel.add(buildBottomPanel(), BorderLayout.SOUTH); + } + + /** Checkboxes + search bar */ + private JPanel buildTopPanel() { + JPanel top = new JPanel(new BorderLayout(4, 2)); + top.setBorder(BorderFactory.createEmptyBorder(4, 6, 2, 6)); + + // Checkbox row + JPanel checkboxRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 0)); + limitToFunctionCheckbox = new JCheckBox("Limit results to current function"); + showByFunctionCheckbox = new JCheckBox("Show matches by function"); + limitToFunctionCheckbox.setFocusable(false); + showByFunctionCheckbox.setFocusable(false); + + limitToFunctionCheckbox.addActionListener(e -> refreshFilter()); + showByFunctionCheckbox.addActionListener(e -> refreshFilter()); + + checkboxRow.add(limitToFunctionCheckbox); + checkboxRow.add(showByFunctionCheckbox); + + // Search row + searchField = new JTextField(); + searchField.putClientProperty("JTextField.placeholderText", "search..."); + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { refreshFilter(); } + @Override public void removeUpdate(DocumentEvent e) { refreshFilter(); } + @Override public void changedUpdate(DocumentEvent e) { refreshFilter(); } + }); + + top.add(checkboxRow, BorderLayout.NORTH); + top.add(searchField, BorderLayout.CENTER); + return top; + } + + /** The JTreeTable in a scroll pane */ + private JScrollPane buildTreePanel() { + treeTableModel = new CapaResultsTreeModel(); + treeTable = new JTreeTable(treeTableModel); + + treeTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + treeTable.getColumnModel().getColumn(0).setPreferredWidth(380); + treeTable.getColumnModel().getColumn(1).setPreferredWidth(110); + treeTable.getColumnModel().getColumn(2).setPreferredWidth(300); + + // Double-click on Address column navigates to that location + // Attach listener to the JTreeTable for double-click navigation on Address column + treeTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + navigateToSelected(e); + } + } + }); + + scrollPane = new JScrollPane(treeTable); + return scrollPane; + } + + /** Analyze / Reset / Settings buttons + status labels */ + private JPanel buildBottomPanel() { + JPanel bottom = new JPanel(new BorderLayout(0, 2)); + bottom.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6)); + + // Button row + JPanel buttonRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + + analyzeButton = new JButton("Analyze"); + resetButton = new JButton("Reset"); + settingsButton = new JButton("Settings"); + + analyzeButton.addActionListener(e -> onAnalyze()); + resetButton.addActionListener(e -> onReset()); + settingsButton.addActionListener(e -> onSettings()); + + buttonRow.add(analyzeButton); + buttonRow.add(resetButton); + buttonRow.add(settingsButton); + + // Status row + statusLabel = new JLabel(" "); + rulesPathLabel = new JLabel(" "); + rulesPathLabel.setFont(rulesPathLabel.getFont().deriveFont(Font.PLAIN, 11f)); + rulesPathLabel.setForeground(Color.GRAY); + + updateRulesPathLabel(); + + JPanel statusRow = new JPanel(new GridLayout(2, 1, 0, 0)); + statusRow.add(statusLabel); + statusRow.add(rulesPathLabel); + + bottom.add(buttonRow, BorderLayout.NORTH); + bottom.add(statusRow, BorderLayout.SOUTH); + return bottom; + } + + // ------------------------------------------------------------------ // + // Button handlers // + // ------------------------------------------------------------------ // + + private void onAnalyze() { + if (currentProgram == null) { + Msg.showInfo(this, mainPanel, "Capa Analysis", "No program is currently open."); + return; + } + plugin.runAnalysis(false); + } + + private void onReset() { + clearResults(); + setStatus("Results cleared."); + } + + private void onSettings() { + GhidraFileChooser chooser = new GhidraFileChooser(mainPanel); + chooser.setTitle("Select capa rules directory"); + chooser.setApproveButtonText("Select"); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + + // Pre-select existing path if any + String existing = CapaCacheManager.readRulesDirectory(); + if (existing != null && !existing.isEmpty()) { + chooser.setCurrentDirectory(new File(existing)); + } + + File selected = chooser.getSelectedFile(); + chooser.dispose(); + + if (selected != null) { + CapaCacheManager.writeRulesDirectory(selected.getAbsolutePath()); + updateRulesPathLabel(); + setStatus("Rules directory updated."); + } + } + + // ------------------------------------------------------------------ // + // Public display API (called from CapaPlugin / analysis task) // + // ------------------------------------------------------------------ // + + public void showLoading(String message) { + SwingUtilities.invokeLater(() -> { + setStatus(message); + analyzeButton.setEnabled(false); + }); + } + + public void showError(String message) { + SwingUtilities.invokeLater(() -> { + setStatus("Error: " + message); + analyzeButton.setEnabled(true); + }); + } + + public void displayResults(String capaJson) { + SwingUtilities.invokeLater(() -> { + try { + JsonObject json = JsonParser.parseString(capaJson).getAsJsonObject(); + treeTableModel = CapaResultsTreeModel.fromJson(json); + treeTable.setModel(new TreeTableModelAdapter(treeTableModel, treeTable.getTree())); + treeTable.getTree().setModel(treeTableModel); + + // Column widths after model swap + treeTable.getColumnModel().getColumn(0).setPreferredWidth(380); + treeTable.getColumnModel().getColumn(1).setPreferredWidth(110); + treeTable.getColumnModel().getColumn(2).setPreferredWidth(300); + + // Count top-level rules + int ruleCount = treeTableModel.getChildCount(treeTableModel.getRoot()); + // Subtract 1 if root itself is the "capa results" invisible root + setStatus("Analysis complete — " + ruleCount + " rule(s) matched."); + } catch (Exception e) { + showError("Failed to parse results: " + e.getMessage()); + Msg.error(this, "JSON parse error", e); + } + analyzeButton.setEnabled(true); + }); + } + + public void clearResults() { + SwingUtilities.invokeLater(() -> { + treeTableModel = new CapaResultsTreeModel(); + treeTable.setModel(new TreeTableModelAdapter(treeTableModel, treeTable.getTree())); + treeTable.getTree().setModel(treeTableModel); + analyzeButton.setEnabled(true); + }); + } + + // ------------------------------------------------------------------ // + // Program tracking // + // ------------------------------------------------------------------ // + + public void onProgramActivated(Program program) { + this.currentProgram = program; + clearResults(); + setStatus(program != null ? "Ready — click Analyze to run capa." : "No program open."); + } + + public void onProgramDeactivated(Program program) { + this.currentProgram = null; + clearResults(); + setStatus("No program open."); + } + + // ------------------------------------------------------------------ // + // Navigation (double-click on address cell) // + // ------------------------------------------------------------------ // + + /** + * Navigate to the address when user double-clicks the Address column. + * This replicates the behavior of the capa IDA plugin. + */ + private void navigateToSelected(MouseEvent e) { + // Get the row and column at the click point using table coordinates + int row = treeTable.rowAtPoint(e.getPoint()); + int column = treeTable.columnAtPoint(e.getPoint()); + + Msg.info(this, "Double-click detected at point: " + e.getPoint()); + Msg.info(this, "Row index: " + row + ", Column index: " + column); + + // Only navigate when clicking the Address column (column 1) + if (row < 0 || column != 1) { + Msg.info(this, "Ignoring click: row=" + row + " is invalid or column=" + column + " is not Address column"); + return; + } + + // Read the address string from the Address column (column 1) + Object addressVal = treeTable.getValueAt(row, 1); + Msg.info(this, "Address value at [" + row + ", 1]: " + addressVal + + " (type: " + (addressVal != null ? addressVal.getClass().getName() : "null") + ")"); + + if (addressVal == null || !(addressVal instanceof String)) { + Msg.info(this, "Address value is not a string or is null"); + return; + } + + String addrStr = ((String) addressVal).trim(); + if (addrStr.isEmpty()) { + Msg.info(this, "Address string is empty after trim"); + return; + } + + Msg.info(this, "Attempting to navigate to address: " + addrStr); + + try { + // Parse hex address (handles both "0x4019F0" and "4019F0" formats) + long offset = Long.parseUnsignedLong( + addrStr.startsWith("0x") || addrStr.startsWith("0X") + ? addrStr.substring(2) : addrStr, 16); + + Msg.info(this, "Successfully parsed offset: 0x" + Long.toHexString(offset)); + + // Navigate using Ghidra's GoToService + GoToService goToService = dockingTool.getService(GoToService.class); + if (goToService == null) { + Msg.error(this, "GoToService is null - cannot navigate"); + return; + } + if (currentProgram == null) { + Msg.error(this, "currentProgram is null - cannot navigate"); + return; + } + + Address addr = currentProgram.getAddressFactory() + .getDefaultAddressSpace().getAddress(offset); + + Msg.info(this, "Created Ghidra Address object: " + addr); + goToService.goTo(addr); + Msg.info(this, "Navigation successful to " + addr); + } catch (NumberFormatException ex) { + Msg.error(this, "Failed to parse address '" + addrStr + "' as hex", ex); + } catch (Exception ex) { + Msg.error(this, "Navigation failed", ex); + } + } + + // ------------------------------------------------------------------ // + // Filter (search / checkboxes) — placeholder, expand as needed // + // ------------------------------------------------------------------ // + + private void refreshFilter() { + // TODO: implement live filtering of tree rows based on searchField text + // and the two checkboxes. For now just a no-op so the UI is wired. + } + + // ------------------------------------------------------------------ // + // Helpers // + // ------------------------------------------------------------------ // + + private void setStatus(String msg) { + statusLabel.setText(msg); + } + + private void updateRulesPathLabel() { + String rulesDir = CapaCacheManager.readRulesDirectory(); + if (rulesDir == null || rulesDir.isEmpty()) { + rulesPathLabel.setText("capa rules directory: (not configured — click Settings)"); + } else { + // Count yml files in root to mimic "474 rules" + File dir = new File(rulesDir); + int ruleCount = countYmlFiles(dir); + rulesPathLabel.setText("capa rules directory: " + rulesDir + + (ruleCount > 0 ? " (" + ruleCount + " rules)" : "")); + } + } + + private int countYmlFiles(File dir) { + if (dir == null || !dir.isDirectory()) return 0; + int count = 0; + File[] files = dir.listFiles(); + if (files == null) return 0; + for (File f : files) { + if (f.isDirectory()) { + count += countYmlFiles(f); + } else if (f.getName().endsWith(".yml") || f.getName().endsWith(".yaml")) { + count++; + } + } + return count; + } +} \ No newline at end of file diff --git a/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaResults.java b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaResults.java new file mode 100644 index 0000000000..4aef58f265 --- /dev/null +++ b/capa/ghidra/plugin/extension/src/main/java/capa/ghidra/CapaResults.java @@ -0,0 +1,84 @@ +package capa.ghidra; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * Legacy data class - kept for cache compatibility. + * New code uses CapaTreeTableModel to parse JSON directly. + */ +public class CapaResults { + + private static final Gson gson = new Gson(); + + public String programName; + public String programPath; + public String imageBase; + public String language; + public String compiler; + + public int functionCount; + public int externalFunctionCount; + public String timestamp; + public String capaVersion; + public String programHash; + + public List