Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -15,6 +15,7 @@
### capa Explorer Web

### capa Explorer IDA Pro plugin
- ida plugin: add a font explorer in settings @vee1e #2995

### Development
- ci: use explicit and per job permissions @mike-hunhoff #3002
Expand Down Expand Up @@ -165,7 +166,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
112 changes: 99 additions & 13 deletions capa/ida/plugin/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,34 @@
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__}"


def get_configured_font() -> QtGui.QFont:
"""return the saved font or fall back to the system fixed font"""
font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
font_str = settings.user.get(CAPA_SETTINGS_FONT, "")
if font_str:
font.fromString(font_str)
return font


def get_scaled_ui_font(font: QtGui.QFont) -> QtGui.QFont:
"""return the default UI font scaled to the configured point size"""
ui_font = QtGui.QFont()
if font.pointSize() > 0:
ui_font.setPointSize(font.pointSize())
return ui_font


def get_default_ui_font() -> QtGui.QFont:
"""return the default UI font without any user scaling"""
return QtGui.QFont()


CAPA_RULESET_DOC_URL = "https://github.com/mandiant/capa/blob/master/doc/rules.md"


Expand Down Expand Up @@ -117,10 +142,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 +157,11 @@ 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 = get_configured_font()

self.edit_rules_link.setText(
f'<a href="{CAPA_OFFICIAL_RULESET_URL}">Download and extract official capa rules</a>'
Expand All @@ -154,6 +183,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 +197,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 @@ -228,6 +275,8 @@ def __init__(self, name: str, option=Options.NO_ANALYSIS):
self.view_rulegen_preview: CapaExplorerRulegenPreview
self.view_rulegen_features: CapaExplorerRulegenFeatures
self.view_rulegen_editor: CapaExplorerRulegenEditor
self.view_rulegen_preview_label: QtWidgets.QLabel
self.view_rulegen_editor_label: QtWidgets.QLabel
self.view_rulegen_header_label: QtWidgets.QLabel
self.view_rulegen_search: QtWidgets.QLineEdit
self.view_rulegen_limit_features_by_ea: QtWidgets.QCheckBox
Expand Down Expand Up @@ -304,6 +353,32 @@ 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"""
self.update_fonts(get_configured_font())

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

for component_name in (
"model_data",
"view_tree",
"view_rulegen_preview",
"view_rulegen_editor",
"view_rulegen_features",
):
component = getattr(self, component_name, None)
if component:
component.update_font(font, ui_font)

if hasattr(self, "view_tree") and self.view_tree:
self.view_tree.restore_expanded_source_items(expanded_items)

def load_view_tabs(self):
"""load tabs"""
Expand Down Expand Up @@ -333,6 +408,7 @@ def load_view_status_label(self):
label = QtWidgets.QLabel()
label.setAlignment(QtCore.Qt.AlignLeft)
label.setText(status)
label.setFont(get_default_ui_font())

self.view_status_label_rulegen_cache = status
self.view_status_label_analysis_cache = status
Expand Down Expand Up @@ -368,6 +444,7 @@ def load_view_search_bar(self):
"""load the search bar control"""
line = QtWidgets.QLineEdit()
line.setPlaceholderText("search...")
line.setFont(get_default_ui_font())
line.textChanged.connect(self.slot_limit_results_to_search)

self.view_search_bar = line
Expand Down Expand Up @@ -418,17 +495,16 @@ def load_view_rulegen_tab(self):

font = QtGui.QFont()
font.setBold(True)
font.setPointSize(11)

label1 = QtWidgets.QLabel()
label1.setAlignment(QtCore.Qt.AlignLeft)
label1.setText("Preview")
label1.setFont(font)
self.view_rulegen_preview_label = QtWidgets.QLabel()
self.view_rulegen_preview_label.setAlignment(QtCore.Qt.AlignLeft)
self.view_rulegen_preview_label.setText("Preview")
self.view_rulegen_preview_label.setFont(font)

label2 = QtWidgets.QLabel()
label2.setAlignment(QtCore.Qt.AlignLeft)
label2.setText("Editor")
label2.setFont(font)
self.view_rulegen_editor_label = QtWidgets.QLabel()
self.view_rulegen_editor_label.setAlignment(QtCore.Qt.AlignLeft)
self.view_rulegen_editor_label.setText("Editor")
self.view_rulegen_editor_label.setFont(font)

self.view_rulegen_limit_features_by_ea = QtWidgets.QCheckBox("Limit features to current disassembly address")
self.view_rulegen_limit_features_by_ea.setChecked(False)
Expand All @@ -437,10 +513,12 @@ def load_view_rulegen_tab(self):
self.view_rulegen_status_label = QtWidgets.QLabel()
self.view_rulegen_status_label.setAlignment(QtCore.Qt.AlignLeft)
self.view_rulegen_status_label.setText("")
self.view_rulegen_status_label.setFont(get_default_ui_font())

self.view_rulegen_search = QtWidgets.QLineEdit()
self.view_rulegen_search.setPlaceholderText("search...")
self.view_rulegen_search.setClearButtonEnabled(True)
self.view_rulegen_search.setFont(get_default_ui_font())
self.view_rulegen_search.textChanged.connect(self.slot_limit_rulegen_features_to_search)

self.view_rulegen_header_label = QtWidgets.QLabel()
Expand All @@ -457,10 +535,10 @@ def load_view_rulegen_tab(self):

self.set_rulegen_preview_border_neutral()

layout1.addWidget(label1)
layout1.addWidget(self.view_rulegen_preview_label)
layout1.addWidget(self.view_rulegen_preview, 45)
layout1.addWidget(self.view_rulegen_status_label)
layout3.addWidget(label2)
layout3.addWidget(self.view_rulegen_editor_label)
layout3.addWidget(self.view_rulegen_editor, 65)

layout2.addWidget(self.view_rulegen_header_label)
Expand Down Expand Up @@ -1301,14 +1379,22 @@ def slot_save(self):

def slot_settings(self):
""" """
dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent)
original_font = get_configured_font()

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
23 changes: 21 additions & 2 deletions capa/ida/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ 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)
self.current_ui_font = QtGui.QFont()

def update_font(self, font: QtGui.QFont, ui_font: Optional[QtGui.QFont] = None):
"""update the font used to render items"""
self.beginResetModel()
self.current_font = font
self.current_ui_font = QtGui.QFont(self.current_ui_font if ui_font is None else ui_font)
self.endResetModel()

def reset(self):
"""reset UI elements (e.g. checkboxes, IDA color highlights)
Expand Down Expand Up @@ -134,7 +143,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,7 +166,7 @@ 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_ui_font)
font.setBold(True)
return font

Expand Down Expand Up @@ -244,6 +254,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