diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index bf420c7e7..212d8b908 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -37,6 +37,10 @@ logger = logging.getLogger(__name__) settings = ida_settings.IDASettings("capa") +CAPA_SETTINGS_RULE_PATH = "rule_path" +CAPA_SETTINGS_RULEGEN_AUTHOR = "rulegen_author" +CAPA_SETTINGS_RULEGEN_SCOPE = "rulegen_scope" + def write_file(path, data): """ """ @@ -166,6 +170,60 @@ def extract_function_features(self, f): return super(CapaExplorerFeatureExtractor, self).extract_function_features(f) +class QLineEditClicked(QtWidgets.QLineEdit): + def __init__(self, content, parent=None): + """ """ + super(QLineEditClicked, self).__init__(content, parent) + + def mouseReleaseEvent(self, e): + """ """ + old = self.text() + new = str( + QtWidgets.QFileDialog.getExistingDirectory( + self.parent(), "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "") + ) + ) + if new: + self.setText(new) + else: + self.setText(old) + + +class CapaSettingsInputDialog(QtWidgets.QDialog): + def __init__(self, title, parent=None): + """ """ + super(CapaSettingsInputDialog, self).__init__(parent) + + self.setWindowTitle(title) + self.setMinimumWidth(500) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) + + self.edit_rule_path = QLineEditClicked(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")) + self.edit_rule_author = QtWidgets.QLineEdit(settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "")) + self.edit_rule_scope = QtWidgets.QComboBox() + + scopes = ("file", "function", "basic block") + + self.edit_rule_scope.addItems(scopes) + self.edit_rule_scope.setCurrentIndex(scopes.index(settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"))) + + buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self) + + layout = QtWidgets.QFormLayout(self) + layout.addRow("capa rules path", self.edit_rule_path) + layout.addRow("Default rule author", self.edit_rule_author) + layout.addRow("Default rule scope", self.edit_rule_scope) + + layout.addWidget(buttons) + + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + def get_values(self): + """ """ + return self.edit_rule_path.text(), self.edit_rule_author.text(), self.edit_rule_scope.currentText() + + class CapaExplorerForm(idaapi.PluginForm): """form element for plugin interface""" @@ -197,11 +255,11 @@ def __init__(self, name): self.view_rulegen = None self.view_tabs = None self.view_tab_rulegen = None - self.view_menu_bar = None self.view_status_label = None self.view_buttons = None self.view_analyze_button = None self.view_reset_button = None + self.view_settings_button = None self.view_save_button = None self.view_rulegen_preview = None @@ -273,10 +331,6 @@ def load_interface(self): self.load_view_status_label() self.load_view_buttons() - # load menu bar and sub menus - self.load_view_menu_bar() - self.load_configure_menu() - # load parent view self.load_view_parent() @@ -285,11 +339,6 @@ def load_view_tabs(self): tabs = QtWidgets.QTabWidget() self.view_tabs = tabs - def load_view_menu_bar(self): - """load menu bar""" - bar = QtWidgets.QMenuBar() - self.view_menu_bar = bar - def load_view_checkbox_limit_by(self): """load limit results by function checkbox""" check = QtWidgets.QCheckBox("Limit results to current function") @@ -319,19 +368,23 @@ def load_view_buttons(self): analyze_button = QtWidgets.QPushButton("Analyze") reset_button = QtWidgets.QPushButton("Reset") save_button = QtWidgets.QPushButton("Save") + settings_button = QtWidgets.QPushButton("Settings") analyze_button.clicked.connect(self.slot_analyze) reset_button.clicked.connect(self.slot_reset) save_button.clicked.connect(self.slot_save) + settings_button.clicked.connect(self.slot_settings) layout = QtWidgets.QHBoxLayout() layout.addWidget(analyze_button) layout.addWidget(reset_button) - layout.addStretch(2) + layout.addWidget(settings_button) + layout.addStretch(3) layout.addWidget(save_button, alignment=QtCore.Qt.AlignRight) self.view_analyze_button = analyze_button self.view_reset_button = reset_button + self.view_settings_button = settings_button self.view_save_button = save_button self.view_buttons = layout @@ -350,7 +403,6 @@ def load_view_parent(self): layout.addWidget(self.view_tabs) layout.addLayout(self.view_buttons) layout.addWidget(self.view_status_label) - layout.setMenuBar(self.view_menu_bar) self.parent.setLayout(layout) @@ -450,27 +502,6 @@ def load_view_rulegen_tab(self): self.view_tabs.addTab(tab, "Rule Generator") - def load_configure_menu(self): - """ """ - actions = ( - ("Change default rules directory...", "Set default rules directory", self.slot_change_rules_dir), - ("Change default rule author...", "Set default rule author", self.slot_change_rule_author), - ("Change default rule scope...", "Set default rule scope", self.slot_change_rule_scope), - ) - self.load_menu("Settings", actions) - - def load_menu(self, title, actions): - """load menu actions - - @param title: menu name displayed in UI - @param actions: tuple of tuples containing action name, tooltip, and slot function - """ - menu = self.view_menu_bar.addMenu(title) - for (name, _, slot) in actions: - action = QtWidgets.QAction(name, self.parent) - action.triggered.connect(slot) - menu.addAction(action) - def load_ida_hooks(self): """load IDA UI hooks""" # map named action (defined in idagui.cfg) to Python function @@ -567,7 +598,7 @@ def load_capa_rules(self): try: # resolve rules directory - check self and settings first, then ask user - if not os.path.exists(settings.user.get("rule_path", "")): + if not os.path.exists(settings.user.get(CAPA_SETTINGS_RULE_PATH, "")): idaapi.info("Please select a file directory containing capa rules.") path = self.ask_user_directory() if not path: @@ -575,7 +606,7 @@ def load_capa_rules(self): "You must select a file directory containing capa rules before analysis can be run. The standard collection of capa rules can be downloaded from https://github.com/fireeye/capa-rules." ) return False - settings.user["rule_path"] = path + settings.user[CAPA_SETTINGS_RULE_PATH] = path except Exception as e: logger.error("Failed to load capa rules (error: %s).", e) return False @@ -584,7 +615,7 @@ def load_capa_rules(self): logger.info("User cancelled analysis.") return False - rule_path = settings.user["rule_path"] + rule_path = settings.user[CAPA_SETTINGS_RULE_PATH] try: if not os.path.exists(rule_path): raise IOError("rule path %s does not exist or cannot be accessed" % rule_path) @@ -613,7 +644,8 @@ def load_capa_rules(self): total_paths = len(rule_paths) for (i, rule_path) in enumerate(rule_paths): update_wait_box( - "loading capa rules from %s (%d of %d)" % (settings.user["rule_path"], i + 1, total_paths) + "loading capa rules from %s (%d of %d)" + % (settings.user[CAPA_SETTINGS_RULE_PATH], i + 1, total_paths) ) if ida_kernwin.user_cancelled(): raise UserCancelledError("user cancelled") @@ -632,12 +664,14 @@ def load_capa_rules(self): logger.info("User cancelled analysis.") return False except Exception as e: - capa.ida.helpers.inform_user_ida_ui("Failed to load capa rules from %s" % settings.user["rule_path"]) - logger.error("Failed to load rules from %s (error: %s).", settings.user["rule_path"], e) + capa.ida.helpers.inform_user_ida_ui( + "Failed to load capa rules from %s" % settings.user[CAPA_SETTINGS_RULE_PATH] + ) + logger.error("Failed to load rules from %s (error: %s).", settings.user[CAPA_SETTINGS_RULE_PATH], e) logger.error( "Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/fireeye/capa-rules." ) - settings.user["rule_path"] = "" + settings.user[CAPA_SETTINGS_RULE_PATH] = "" return False self.ruleset_cache = ruleset @@ -743,7 +777,7 @@ def slot_progress_feature_extraction(text): try: self.model_data.render_capa_doc(self.doc, self.view_show_results_by_function.isChecked()) self.set_view_status_label( - "capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache)) + "capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache)) ) except Exception as e: logger.error("Failed to render results (error: %s)", e) @@ -881,14 +915,14 @@ def load_capa_function_results(self): # load preview and feature tree self.view_rulegen_preview.load_preview_meta( f.start_ea if f else None, - settings.user.get("rulegen_author", ""), - settings.user.get("rulegen_scope", "function"), + settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, ""), + settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"), ) self.view_rulegen_features.load_features(file_features, func_features) # self.view_rulegen_header_label.setText("Function Features (%s)" % trim_function_name(f)) self.set_view_status_label( - "capa rules directory: %s (%d rules)" % (settings.user["rule_path"], len(self.rules_cache)) + "capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache)) ) except Exception as e: logger.error("Failed to render views (error: %s)" % e) @@ -1066,6 +1100,16 @@ def slot_save(self): elif self.view_tabs.currentIndex() == 1: self.save_function_analysis() + def slot_settings(self): + """ """ + dialog = CapaSettingsInputDialog("capa explorer settings", parent=self.parent) + if dialog.exec_(): + ( + settings.user[CAPA_SETTINGS_RULE_PATH], + settings.user[CAPA_SETTINGS_RULEGEN_AUTHOR], + settings.user[CAPA_SETTINGS_RULEGEN_SCOPE], + ) = dialog.get_values() + def save_program_analysis(self): """ """ if not self.doc: @@ -1143,42 +1187,16 @@ def ask_user_directory(self): """create Qt dialog to ask user for a directory""" return str( QtWidgets.QFileDialog.getExistingDirectory( - self.parent, "Please select a capa rules directory", settings.user.get("rule_path", "") + self.parent, "Please select a capa rules directory", settings.user.get(CAPA_SETTINGS_RULE_PATH, "") ) ) def ask_user_capa_rule_file(self): """ """ return QtWidgets.QFileDialog.getSaveFileName( - None, "Please select a capa rule to edit", settings.user.get("rule_path", ""), "*.yml" + None, "Please select a capa rule to edit", settings.user.get(CAPA_SETTINGS_RULE_PATH, ""), "*.yml" )[0] - def slot_change_rule_scope(self): - """ """ - scope = idaapi.ask_str(str(settings.user.get("rulegen_scope", "function")), 0, "Enter default rule scope") - if scope: - settings.user["rulegen_scope"] = scope - idaapi.info("Run analysis again for your changes to take effect.") - - def slot_change_rule_author(self): - """ """ - author = idaapi.ask_str(str(settings.user.get("rulegen_author", "")), 0, "Enter default rule author") - if author: - settings.user["rulegen_author"] = author - idaapi.info("Run analysis again for your changes to take effect.") - - def slot_change_rules_dir(self): - """allow user to change rules directory - - user selection stored in settings for future runs - """ - path = self.ask_user_directory() - if path: - settings.user["rule_path"] = path - self.rules_cache = None - self.ruleset_cache = None - idaapi.info("Run analysis again for your changes to take effect.") - def set_view_status_label(self, text): """update status label control