diff --git a/capa/ida/plugin/__init__.py b/capa/ida/plugin/__init__.py index 130d4a68..ca52e561 100644 --- a/capa/ida/plugin/__init__.py +++ b/capa/ida/plugin/__init__.py @@ -41,25 +41,20 @@ class CapaExplorerPlugin(idaapi.plugin_t): """called when IDA is loading the plugin""" logging.basicConfig(level=logging.INFO) - # check IDA version and database compatibility + # do not load plugin if IDA version/file type not supported if not is_supported_ida_version(): return idaapi.PLUGIN_SKIP if not is_supported_file_type(): return idaapi.PLUGIN_SKIP - - logger.debug("plugin initialized") - - # plugin is good, but don't keep us in memory return idaapi.PLUGIN_OK def term(self): """called when IDA is unloading the plugin""" - logger.debug("plugin terminated") + pass def run(self, arg): """called when IDA is running the plugin as a script""" self.form = CapaExplorerForm(self.PLUGIN_NAME) - self.form.Show() return True diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index c86dbd0f..edb19788 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -56,27 +56,33 @@ class CapaExplorerForm(idaapi.PluginForm): self.view_attack = None self.view_tabs = None self.view_menu_bar = None - self.view_rules_label = None + self.view_status_label = None + self.view_buttons = None + self.view_analyze_button = None + self.view_reset_button = None + + self.Show() def OnCreate(self, form): - """called when plugin form is created""" + """called when plugin form is created + + load interface and install hooks but do not analyze database + """ self.parent = self.FormToPyQtWidget(form) self.parent.setWindowIcon(QICON) - - # load interface elements self.load_interface() - self.load_capa_results() self.load_ida_hooks() - self.view_tree.reset_ui() - - logger.debug("form created") - def Show(self): """creates form if not already create, else brings plugin to front""" - logger.debug("form show") - return idaapi.PluginForm.Show( - self, self.form_title, options=(idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WCLS_CLOSE_LATER) + return super(CapaExplorerForm, self).Show( + self.form_title, + options=( + idaapi.PluginForm.WOPN_TAB + | idaapi.PluginForm.WOPN_RESTORE + | idaapi.PluginForm.WCLS_CLOSE_LATER + | idaapi.PluginForm.WCLS_SAVE + ), ) def OnClose(self, form): @@ -86,7 +92,6 @@ class CapaExplorerForm(idaapi.PluginForm): """ self.unload_ida_hooks() self.ida_reset() - logger.debug("form closed") def load_interface(self): """load user interface""" @@ -110,17 +115,19 @@ class CapaExplorerForm(idaapi.PluginForm): self.load_view_search_bar() self.load_view_tree_tab() self.load_view_attack_tab() - self.load_view_rules_label() + self.load_view_status_label() + self.load_view_buttons() # load menu bar and sub menus self.load_view_menu_bar() self.load_file_menu() self.load_rules_menu() - self.load_view_menu() # load parent view self.load_view_parent() + self.disable_controls() + def load_view_tabs(self): """load tabs""" tabs = QtWidgets.QTabWidget() @@ -161,12 +168,32 @@ class CapaExplorerForm(idaapi.PluginForm): self.view_limit_results_by_function = check - def load_view_rules_label(self): - """load rules label""" + def load_view_status_label(self): + """load status label""" label = QtWidgets.QLabel() label.setAlignment(QtCore.Qt.AlignLeft) + label.setText("Analyze database to get started...") - self.view_rules_label = label + self.view_status_label = label + + def load_view_buttons(self): + """load the button controls""" + analyze_button = QtWidgets.QPushButton("Analyze") + analyze_button.setToolTip("Run capa analysis on IDB") + reset_button = QtWidgets.QPushButton("Reset") + reset_button.setToolTip("Reset plugin and IDA user interfaces") + + analyze_button.clicked.connect(self.slot_analyze) + reset_button.clicked.connect(self.slot_reset) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(analyze_button) + layout.addWidget(reset_button) + layout.addStretch(1) + + self.view_analyze_button = analyze_button + self.view_reset_button = reset_button + self.view_buttons = layout def load_view_search_bar(self): """load the search bar control""" @@ -181,7 +208,8 @@ class CapaExplorerForm(idaapi.PluginForm): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.view_tabs) - layout.addWidget(self.view_rules_label) + layout.addWidget(self.view_status_label) + layout.addLayout(self.view_buttons) layout.setMenuBar(self.view_menu_bar) self.parent.setLayout(layout) @@ -210,10 +238,7 @@ class CapaExplorerForm(idaapi.PluginForm): def load_file_menu(self): """load file menu controls""" - actions = ( - ("Rerun analysis", "Rerun capa analysis on current database", self.slot_reload), - ("Export results...", "Export capa results as JSON file", self.slot_export_json), - ) + actions = (("Export results...", "Export capa results as JSON file", self.slot_export_json),) self.load_menu("File", actions) def load_rules_menu(self): @@ -221,11 +246,6 @@ class CapaExplorerForm(idaapi.PluginForm): actions = (("Change rules directory...", "Select new rules directory", self.slot_change_rules_dir),) self.load_menu("Rules", actions) - def load_view_menu(self): - """load view menu controls""" - actions = (("Reset view", "Reset plugin view", self.slot_reset),) - self.load_menu("View", actions) - def load_menu(self, title, actions): """load menu actions @@ -261,7 +281,6 @@ class CapaExplorerForm(idaapi.PluginForm): def load_ida_hooks(self): """load IDA UI hooks""" - # map named action (defined in idagui.cfg) to Python function action_hooks = { "MakeName": self.ida_hook_rename, @@ -336,10 +355,12 @@ class CapaExplorerForm(idaapi.PluginForm): """ if post: capa.ida.helpers.inform_user_ida_ui("Running capa analysis again after rebase") - self.slot_reload() + self.slot_analyze() def load_capa_results(self): """run capa analysis and render results in UI""" + # new analysis, new doc + self.doc = None # resolve rules directory - check self and settings first, then ask user if not self.rule_path: @@ -349,31 +370,25 @@ class CapaExplorerForm(idaapi.PluginForm): rule_path = self.ask_user_directory() if not rule_path: capa.ida.helpers.inform_user_ida_ui("You must select a rules directory to use for analysis.") - logger.warning("no rules directory selected. nothing to do.") + logger.warning("no rules loaded. nothing to do") + self.set_view_status_label("No rules loaded") + self.disable_controls() return self.rule_path = rule_path settings.user["rule_path"] = rule_path - logger.debug("-" * 80) - logger.debug(" Using rules from %s.", self.rule_path) - logger.debug(" ") - logger.debug(" You can see the current default rule set here:") - logger.debug(" https://github.com/fireeye/capa-rules") - logger.debug("-" * 80) - try: - rules = capa.main.get_rules(self.rule_path) + rules = capa.main.get_rules(self.rule_path, True) rule_count = len(rules) rules = capa.rules.RuleSet(rules) except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e: capa.ida.helpers.inform_user_ida_ui("Failed to load rules from %s" % self.rule_path) logger.error("failed to load rules from %s (%s)", self.rule_path, e) self.rule_path = "" - self.set_view_rules_label_default() + self.set_view_status_label("No rules loaded") + self.disable_controls() return - self.set_view_rules_label_loaded(self.rule_path, rule_count) - meta = capa.ida.helpers.collect_metadata() capabilities, counts = capa.main.find_capabilities( @@ -402,15 +417,12 @@ class CapaExplorerForm(idaapi.PluginForm): if capa.main.has_file_limitation(rules, capabilities, is_standalone=False): capa.ida.helpers.inform_user_ida_ui("capa encountered warnings during analysis") - logger.debug("analysis completed.") - self.doc = capa.render.convert_capabilities_to_result_document(meta, rules, capabilities) - - # render views self.model_data.render_capa_doc(self.doc) self.render_capa_doc_mitre_summary() - logger.debug("render views completed.") + self.enable_controls() + self.set_view_status_label("Loaded %d rules from %s" % (rule_count, self.rule_path)) def render_capa_doc_mitre_summary(self): """render MITRE ATT&CK results""" @@ -465,24 +477,12 @@ class CapaExplorerForm(idaapi.PluginForm): @param text: header text to display """ item = QtWidgets.QTableWidgetItem(text) - item.setForeground(QtGui.QColor(88, 139, 174)) + item.setForeground(QtGui.QColor(37, 147, 215)) font = QtGui.QFont() font.setBold(True) item.setFont(font) return item - def set_view_rules_label_default(self): - """set view rules label to default default text""" - self.view_rules_label.setText("No rules loaded") - - def set_view_rules_label_loaded(self, path, count): - """set view rules label to rule path/count - - @param path: rule path - @param count: number of rules loaded from path - """ - self.view_rules_label.setText("Loaded %d rules from %s" % (count, path)) - def ida_reset(self): """reset plugin UI @@ -493,8 +493,8 @@ class CapaExplorerForm(idaapi.PluginForm): self.model_data.reset() self.view_tree.reset_ui() - def slot_reload(self): - """re-run capa analysis and reload UI controls + def slot_analyze(self): + """run capa analysis and reload UI controls called when user selects plugin reload from menu """ @@ -502,11 +502,8 @@ class CapaExplorerForm(idaapi.PluginForm): self.search_model_proxy.invalidate() self.model_data.clear() self.load_capa_results() - self.ida_reset() - - logger.debug("%s reload completed", self.form_title) - idaapi.info("%s reload completed." % self.form_title) + logger.info("analysis complete") def slot_reset(self, checked): """reset UI elements @@ -514,20 +511,7 @@ class CapaExplorerForm(idaapi.PluginForm): e.g. checkboxes and IDA highlighting """ self.ida_reset() - - logger.debug("%s reset completed", self.form_title) - idaapi.info("%s reset completed" % self.form_title) - - def slot_menu_bar_hovered(self, action): - """display menu action tooltip - - @param action: QtWidgets.QAction* - - @reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu - """ - QtWidgets.QToolTip.showText( - QtGui.QCursor.pos(), action.toolTip(), self.view_menu_bar, self.view_menu_bar.actionGeometry(action) - ) + logger.info("reset complete") def slot_checkbox_limit_by_changed(self, state): """slot activated if checkbox clicked @@ -583,4 +567,23 @@ class CapaExplorerForm(idaapi.PluginForm): settings.user["rule_path"] = rule_path if 1 == idaapi.ask_yn(1, "Run analysis now?"): - self.slot_reload() + self.slot_analyze() + + def set_view_status_label(self, text): + """update status label control + + @param text: updated text + """ + self.view_status_label.setText(text) + + def disable_controls(self): + """disable form controls""" + self.view_reset_button.setEnabled(False) + self.view_tabs.setTabEnabled(0, False) + self.view_tabs.setTabEnabled(1, False) + + def enable_controls(self): + """enable form controls""" + self.view_reset_button.setEnabled(True) + self.view_tabs.setTabEnabled(0, True) + self.view_tabs.setTabEnabled(1, True) diff --git a/capa/ida/plugin/model.py b/capa/ida/plugin/model.py index 6e9c4806..4626a01d 100644 --- a/capa/ida/plugin/model.py +++ b/capa/ida/plugin/model.py @@ -29,7 +29,7 @@ from capa.ida.plugin.item import ( ) # default highlight color used in IDA window -DEFAULT_HIGHLIGHT = 0xD096FF +DEFAULT_HIGHLIGHT = 0xE6C700 class CapaExplorerDataModel(QtCore.QAbstractItemModel): @@ -144,7 +144,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel): if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS: # set color for virtual address column - return QtGui.QColor(88, 139, 174) + return QtGui.QColor(37, 147, 215) if ( role == QtCore.Qt.ForegroundRole