Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5974440
build(deps): bump rich from 14.0.0 to 14.2.0
dependabot[bot] Oct 13, 2025
3bc2d99
Sync capa-testfiles submodule
capa-bot Oct 13, 2025
7897472
Merge pull request #2728 from mandiant/dependabot/pip/rich-14.2.0
mr-tz Oct 16, 2025
da0803b
build(deps-dev): bump vite from 6.3.4 to 6.4.0 in /web/explorer
dependabot[bot] Oct 16, 2025
0099e75
binja: fix crash in binja feature extraction when MLIL is unavailable…
xusheng6 Oct 20, 2025
acb34e8
Update CHANGELOG.md
mike-hunhoff Oct 20, 2025
add09df
Sync capa-testfiles submodule
capa-bot Oct 20, 2025
e6df6ad
Sync capa rules submodule
capa-bot Oct 20, 2025
08319f5
Merge pull request #2730 from mandiant/dependabot/npm_and_yarn/web/ex…
mr-tz Oct 20, 2025
5906bb3
build(deps-dev): bump vite from 6.4.0 to 6.4.1 in /web/explorer
dependabot[bot] Oct 21, 2025
82cbfd3
Merge pull request #2732 from xusheng6/test_fix_binja_crash
mr-tz Oct 24, 2025
4dbdd9d
Merge branch 'master' into dependabot/npm_and_yarn/web/explorer/vite-…
mr-tz Oct 24, 2025
5a0c474
Merge pull request #2735 from mandiant/dependabot/npm_and_yarn/web/ex…
mr-tz Oct 24, 2025
41dd971
ida: add Qt compatibility layer for PyQt5 and PySide6
williballenthin Oct 9, 2025
f1f4753
gitignore uv.lock
williballenthin Oct 9, 2025
f4cc0e4
update dependencies
mr-tz Oct 27, 2025
68cf74d
Sync capa rules submodule
capa-bot Oct 28, 2025
ca708ca
Sync capa-testfiles submodule
capa-bot Oct 28, 2025
6795813
Sync capa rules submodule
capa-bot Oct 28, 2025
b00d9b9
Merge pull request #2 from mandiant/wb/terragon/detect-qt-version-log…
williballenthin Oct 29, 2025
990c807
Merge branch 'master' into terragon/detect-qt-version-logic-rf55m4
mr-tz Oct 29, 2025
e328893
add ruff noqa
mr-tz Oct 29, 2025
62c469b
Merge pull request #3 from mandiant/wb/terragon/detect-qt-version-log…
williballenthin Oct 29, 2025
5ea6377
Merge pull request #2724 from HexRays-plugin-contributions/ida-plugin…
williballenthin Oct 29, 2025
fb49592
Merge branch 'master' into terragon/detect-qt-version-logic-rf55m4
mr-tz Oct 29, 2025
8d69720
qt_compat: use __all__ rather than noqa
williballenthin Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[tool.bumpversion]
current_version = "9.2.1"

[[tool.bumpversion.files]]
filename = "capa/version.py"
search = '__version__ = "{current_version}"'
replace = '__version__ = "{new_version}"'

[[tool.bumpversion.files]]
filename = "capa/ida/plugin/ida-plugin.json"
search = '"version": "{current_version}"'
replace = '"version": "{new_version}"'

[[tool.bumpversion.files]]
filename = "capa/ida/plugin/ida-plugin.json"
search = '"flare-capa=={current_version}"'
replace = '"flare-capa=={new_version}"'

[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
search = "v{current_version}...master"
replace = "{current_version}...{new_version}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ scripts/perf/*.zip
*/.DS_Store
Pipfile
Pipfile.lock
uv.lock
/cache/
.github/binja/binaryninja
.github/binja/download_headless.py
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

### Breaking Changes

### New Rules (19)
### New Rules (21)

- anti-analysis/anti-vm/vm-detection/detect-mouse-movement-via-activity-checks-on-windows tevajdr@gmail.com
- nursery/create-executable-heap moritz.raabe@mandiant.com
Expand All @@ -27,17 +27,25 @@
- host-interaction/network/enumerate-tcp-connections-via-wmi-com-api jakubjozwiak@google.com
- host-interaction/network/routing-table/create-routing-table-entry jakubjozwiak@google.com
- host-interaction/network/routing-table/get-routing-table michael.hunhoff@mandiant.com
- host-interaction/file-system/use-io_uring-io-interface-on-linux jakubjozwiak@google.com
- collection/keylog/log-keystrokes-via-direct-input zeze-zeze
-

### Bug Fixes

- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714

### capa Explorer Web

### capa Explorer IDA Pro plugin

- add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin
- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707

### Development

- ci: remove redundant "test_run" action from build workflow @mike-hunhoff #2692
- dev: add bumpmyversion to bump and sync versions across the project @mr-tz

### Raw diffs
- [capa v9.2.1...master](https://github.com/mandiant/capa/compare/v9.2.1...master)
Expand Down
6 changes: 2 additions & 4 deletions capa/features/extractors/binja/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
Function,
BinaryView,
SymbolType,
ILException,
RegisterValueType,
VariableSourceType,
LowLevelILOperation,
Expand Down Expand Up @@ -192,9 +191,8 @@ def extract_stackstring(fh: FunctionHandle):
if bv is None:
return

try:
mlil = func.mlil
except ILException:
mlil = func.mlil
if mlil is None:
return

for block in mlil.basic_blocks:
Expand Down
4 changes: 2 additions & 2 deletions capa/ida/plugin/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@


import ida_kernwin
from PyQt5 import QtCore

from capa.ida.plugin.error import UserCancelledError
from capa.ida.plugin.qt_compat import QtCore, Signal
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
from capa.features.extractors.base_extractor import FunctionHandle


class CapaExplorerProgressIndicator(QtCore.QObject):
"""implement progress signal, used during feature extraction"""

progress = QtCore.pyqtSignal(str)
progress = Signal(str)

def update(self, text):
"""emit progress update
Expand Down
6 changes: 3 additions & 3 deletions capa/ida/plugin/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import idaapi
import ida_kernwin
import ida_settings
from PyQt5 import QtGui, QtCore, QtWidgets

import capa.main
import capa.rules
Expand Down Expand Up @@ -51,6 +50,7 @@
from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
from capa.features.extractors.base_extractor import FunctionHandle

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -1358,7 +1358,7 @@ def slot_checkbox_limit_by_changed(self, state):

@param state: checked state
"""
if state == QtCore.Qt.Checked:
if state:
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
else:
self.range_model_proxy.reset_address_range_filter()
Expand All @@ -1367,7 +1367,7 @@ def slot_checkbox_limit_by_changed(self, state):

def slot_checkbox_limit_features_by_ea(self, state):
""" """
if state == QtCore.Qt.Checked:
if state:
self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea())
else:
self.view_rulegen_features.show_all_items()
Expand Down
38 changes: 38 additions & 0 deletions capa/ida/plugin/ida-plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"IDAMetadataDescriptorVersion": 1,
"plugin": {
"name": "capa",
"entryPoint": "capa_explorer.py",
"version": "9.2.1",
"idaVersions": ">=7.4",
"description": "Identify capabilities in executable files using FLARE's capa framework",
"license": "Apache-2.0",
"categories": [
"malware-analysis",
"api-scripting-and-automation",
"ui-ux-and-visualization"
],
"pythonDependencies": ["flare-capa==9.2.1"],
"urls": {
"repository": "https://github.com/mandiant/capa"
},
"authors": [
{"name": "Willi Ballenthin", "email": "wballenthin@hex-rays.com"},
{"name": "Moritz Raabe", "email": "moritzraabe@google.com"},
{"name": "Mike Hunhoff", "email": "mike.hunhoff@gmail.com"},
{"name": "Yacine Elhamer", "email": "elhamer.yacine@gmail.com"}
],
"keywords": [
"capability-detection",
"malware-analysis",
"behavior-analysis",
"reverse-engineering",
"att&ck",
"rule-engine",
"feature-extraction",
"yara-like-rules",
"static-analysis",
"dynamic-analysis"
]
}
}
4 changes: 2 additions & 2 deletions capa/ida/plugin/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import idc
import idaapi
from PyQt5 import QtCore

import capa.ida.helpers
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtCore, qt_get_item_flag_tristate


def info_to_name(display):
Expand Down Expand Up @@ -55,7 +55,7 @@ def __init__(self, parent: Optional["CapaExplorerDataItem"], data: list[str], ca
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

if self._can_check:
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | qt_get_item_flag_tristate()

if self.pred:
self.pred.appendChild(self)
Expand Down
4 changes: 2 additions & 2 deletions capa/ida/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import idc
import idaapi
from PyQt5 import QtGui, QtCore

import capa.rules
import capa.ida.helpers
Expand All @@ -42,6 +41,7 @@
CapaExplorerInstructionViewItem,
)
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtGui, QtCore

# default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xE6C700
Expand Down Expand Up @@ -269,7 +269,7 @@ def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True):
visited.add(child_index)

for idx in range(self.rowCount(child_index)):
stack.append(child_index.child(idx, 0))
stack.append(self.index(idx, 0, child_index))

def reset_ida_highlighting(self, item, checked):
"""reset IDA highlight for item
Expand Down
4 changes: 1 addition & 3 deletions capa/ida/plugin/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from PyQt5 import QtCore
from PyQt5.QtCore import Qt

from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import Qt, QtCore


class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
Expand Down
79 changes: 79 additions & 0 deletions capa/ida/plugin/qt_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright 2020 Google LLC
#
# 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.

"""
Qt compatibility layer for capa IDA Pro plugin.

Handles PyQt5 (IDA < 9.2) vs PySide6 (IDA >= 9.2) differences.
This module provides a unified import interface for Qt modules and handles
API changes between Qt5 and Qt6.
"""

try:
# IDA 9.2+ uses PySide6
from PySide6 import QtGui, QtCore, QtWidgets
from PySide6.QtGui import QAction

QT_LIBRARY = "PySide6"
Signal = QtCore.Signal
except ImportError:
# Older IDA versions use PyQt5
try:
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import QAction

QT_LIBRARY = "PyQt5"
Signal = QtCore.pyqtSignal
except ImportError:
raise ImportError("Neither PySide6 nor PyQt5 is available. Cannot initialize capa IDA plugin.")

Qt = QtCore.Qt


def qt_get_item_flag_tristate():
"""
Get the tristate item flag compatible with Qt5 and Qt6.

Qt5 (PyQt5): Uses Qt.ItemIsTristate
Qt6 (PySide6): Qt.ItemIsTristate was removed, uses Qt.ItemIsAutoTristate

ItemIsAutoTristate automatically manages tristate based on child checkboxes,
matching the original ItemIsTristate behavior where parent checkboxes reflect
the check state of their children.

Returns:
int: The appropriate flag value for the Qt version

Raises:
AttributeError: If the tristate flag cannot be found in the Qt library
"""
if QT_LIBRARY == "PySide6":
# Qt6: ItemIsTristate was removed, replaced with ItemIsAutoTristate
# Try different possible locations (API varies slightly across PySide6 versions)
if hasattr(Qt, "ItemIsAutoTristate"):
return Qt.ItemIsAutoTristate
elif hasattr(Qt, "ItemFlag") and hasattr(Qt.ItemFlag, "ItemIsAutoTristate"):
return Qt.ItemFlag.ItemIsAutoTristate
else:
raise AttributeError(
"Cannot find ItemIsAutoTristate in PySide6. "
+ "Your PySide6 version may be incompatible with capa. "
+ f"Available Qt attributes: {[attr for attr in dir(Qt) if 'Item' in attr]}"
)
else:
# Qt5: Use the original ItemIsTristate flag
return Qt.ItemIsTristate


__all__ = ["qt_get_item_flag_tristate", "Signal", "QAction", "QtGui", "QtCore", "QtWidgets"]
6 changes: 3 additions & 3 deletions capa/ida/plugin/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import idc
import idaapi
from PyQt5 import QtGui, QtCore, QtWidgets

import capa.rules
import capa.engine
Expand All @@ -28,6 +27,7 @@
from capa.ida.plugin.item import CapaExplorerFunctionItem
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import QtGui, QtCore, Signal, QAction, QtWidgets

MAX_SECTION_SIZE = 750

Expand Down Expand Up @@ -147,7 +147,7 @@ def calc_item_depth(o):

def build_action(o, display, data, slot):
""" """
action = QtWidgets.QAction(display, o)
action = QAction(display, o)

action.setData(data)
action.triggered.connect(lambda checked: slot(action))
Expand Down Expand Up @@ -312,7 +312,7 @@ def set_selection(self, start, end, max):


class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
updated = QtCore.pyqtSignal()
updated = Signal()

def __init__(self, preview, parent=None):
""" """
Expand Down
2 changes: 1 addition & 1 deletion doc/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [ ] Review changes
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
- [ ] Run `$ bump-my-version bump {patch/minor/major} [--allow-dirty]` to update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py) and other version files
- [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md)
- Do not forget to add a nice introduction thanking contributors
- Remember that we need a major release if we introduce breaking changes
Expand Down Expand Up @@ -36,7 +37,6 @@
- [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master)
- [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master)
```
- [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py)
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
- [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section)
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md).
Expand Down
Loading
Loading