Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## master (unreleased)

### New Features
- ida plugin: add a "Font…" action so users can adjust the explorer font on demand, with desktop DPI awareness baked in #2570

### Breaking Changes

Expand Down Expand Up @@ -163,7 +164,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR

### Bug Fixes

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

### capa Explorer Web

Expand Down
71 changes: 67 additions & 4 deletions capa/ida/plugin/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author"
CAPA_SETTINGS_RULEGEN_SCOPE = "rulegen_scope"
CAPA_SETTINGS_ANALYZE = "analyze"
CAPA_SETTINGS_FONT = "font"


CAPA_OFFICIAL_RULESET_URL = f"https://github.com/mandiant/capa-rules/releases/tag/v{capa.version.__version__}"
Expand Down Expand Up @@ -117,10 +118,12 @@ def mouseReleaseEvent(self, e):


class CapaSettingsInputDialog(QtWidgets.QDialog):
def __init__(self, title, parent=None):
def __init__(self, title, parent=None, on_font_changed=None):
""" """
super().__init__(parent)

self.on_font_changed = on_font_changed

self.setWindowTitle(title)
self.setMinimumWidth(500)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
Expand All @@ -130,9 +133,14 @@ def __init__(self, title, parent=None):
self.edit_rule_scope = QtWidgets.QComboBox()
self.edit_rules_link = QtWidgets.QLabel()
self.edit_analyze = QtWidgets.QComboBox()
self.btn_font = QtWidgets.QPushButton("Font")
self.btn_delete_results = QtWidgets.QPushButton(
self.style().standardIcon(QtWidgets.QStyle.SP_BrowserStop), "Delete cached capa results"
)
self.font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
font_str = settings.user.get(CAPA_SETTINGS_FONT, "")
if font_str:
self.font.fromString(font_str)

self.edit_rules_link.setText(
f'<a href="{CAPA_OFFICIAL_RULESET_URL}">Download and extract official capa rules</a>'
Expand All @@ -154,6 +162,7 @@ def __init__(self, title, parent=None):
layout.addRow("", self.edit_rules_link)

layout.addRow("Plugin start option", self.edit_analyze)
layout.addRow("Explorer font", self.btn_font)
if capa.ida.helpers.idb_contains_cached_results():
self.btn_delete_results.clicked.connect(capa.ida.helpers.delete_cached_results)
self.btn_delete_results.clicked.connect(lambda state: self.btn_delete_results.setEnabled(False))
Expand All @@ -167,16 +176,33 @@ def __init__(self, title, parent=None):

layout.addWidget(buttons)

self.btn_font.clicked.connect(self.select_font)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)

def select_font(self):
"""launch the font dialog"""
original_font = QtGui.QFont(self.font)
dialog = QtWidgets.QFontDialog(self.font, self)
dialog.setWindowTitle("Select Plugin Font")
if self.on_font_changed:
dialog.currentFontChanged.connect(self.on_font_changed)

if dialog.exec_():
self.font = dialog.currentFont()
if self.on_font_changed:
self.on_font_changed(self.font)
elif self.on_font_changed:
self.on_font_changed(original_font)

def get_values(self):
""" """
return (
self.edit_rule_path.text(),
self.edit_rule_author.text(),
self.edit_rule_scope.currentText(),
self.edit_analyze.currentIndex(),
self.font.toString(),
)


Expand Down Expand Up @@ -304,6 +330,33 @@ def load_interface(self):

# load parent view
self.load_view_parent()
self.load_font()

def load_font(self):
"""load the user-configured font or fall back to the system fixed font"""
font_str = settings.user.get(CAPA_SETTINGS_FONT, "")
font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
if font_str:
font.fromString(font_str)
self.update_fonts(font)
Comment thread
vee1e marked this conversation as resolved.
Outdated

def update_fonts(self, font: QtGui.QFont):
"""propagate the selected font throughout the plugin UI"""
expanded_items = []
if hasattr(self, "view_tree") and self.view_tree:
expanded_items = self.view_tree.get_expanded_source_items()

if hasattr(self, "model_data") and self.model_data:
self.model_data.update_font(font)
if hasattr(self, "view_tree") and self.view_tree:
self.view_tree.update_font(font)
self.view_tree.restore_expanded_source_items(expanded_items)
if hasattr(self, "view_rulegen_preview") and self.view_rulegen_preview:
self.view_rulegen_preview.update_font(font)
if hasattr(self, "view_rulegen_editor") and self.view_rulegen_editor:
self.view_rulegen_editor.update_font(font)
if hasattr(self, "view_rulegen_features") and self.view_rulegen_features:
self.view_rulegen_features.update_font(font)
Comment thread
vee1e marked this conversation as resolved.
Outdated

def load_view_tabs(self):
"""load tabs"""
Expand Down Expand Up @@ -416,9 +469,8 @@ def load_view_rulegen_tab(self):
left = QtWidgets.QWidget()
left.setLayout(layout2)

font = QtGui.QFont()
font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
font.setBold(True)
Comment thread
vee1e marked this conversation as resolved.
Outdated
font.setPointSize(11)

label1 = QtWidgets.QLabel()
label1.setAlignment(QtCore.Qt.AlignLeft)
Expand Down Expand Up @@ -1301,14 +1353,25 @@ def slot_save(self):

def slot_settings(self):
""" """
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
original_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
font_str = settings.user.get(CAPA_SETTINGS_FONT, "")
if font_str:
original_font.fromString(font_str)

dialog = CapaSettingsInputDialog(
"capa explorer settings", parent=self.parent, on_font_changed=self.update_fonts
)
if dialog.exec_():
(
settings.user[CAPA_SETTINGS_RULE_PATH],
settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR],
settings.user[CAPA_SETTINGS_RULEGEN_SCOPE],
settings.user[CAPA_SETTINGS_ANALYZE],
settings.user[CAPA_SETTINGS_FONT],
) = dialog.get_values()
self.load_font()
else:
self.update_fonts(original_font)

def save_program_analysis(self):
""" """
Expand Down
24 changes: 22 additions & 2 deletions capa/ida/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def __init__(self, parent=None):
super().__init__(parent)
# root node does not have parent, contains header columns
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
self.current_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)

def update_font(self, font: QtGui.QFont):
"""update the font used to render items"""
self.beginResetModel()
self.current_font = font
self.endResetModel()

def reset(self):
"""reset UI elements (e.g. checkboxes, IDA color highlights)
Expand Down Expand Up @@ -134,7 +141,8 @@ def data(self, model_index, role):
CapaExplorerDataModel.COLUMN_INDEX_DETAILS,
):
# set font for virtual address and details columns
font = QtGui.QFont("Courier", weight=QtGui.QFont.Medium)
font = QtGui.QFont(self.current_font)
font.setWeight(QtGui.QFont.Medium)
if column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS:
font.setBold(True)
return font
Expand All @@ -156,10 +164,13 @@ def data(self, model_index, role):
and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION
):
# set bold font for important items
font = QtGui.QFont()
font = QtGui.QFont(self.current_font)
font.setBold(True)
return font

if role == QtCore.Qt.FontRole:
return QtGui.QFont(self.current_font)

if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS:
# set color for virtual address column
return QtGui.QColor(37, 147, 215)
Expand Down Expand Up @@ -244,6 +255,15 @@ def parent(self, model_index):

return self.createIndex(parent.row(), 0, parent)

def index_from_item(self, item, column=0):
"""return the model index for the given item"""
if item is None or item == self.root_node:
return QtCore.QModelIndex()

parent = item.parent()
parent_index = self.index_from_item(parent, 0)
return self.index(item.row(), column, parent_index)

def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True):
"""depth-first traversal of child nodes

Expand Down
Loading
Loading