Merge pull request #296 from fireeye/explorer-documentation-updates

This commit is contained in:
mike-hunhoff
2020-09-08 12:42:12 -06:00
committed by GitHub
8 changed files with 338 additions and 247 deletions

View File

@@ -34,13 +34,11 @@ class CapaExplorerPlugin(idaapi.plugin_t):
flags = 0
def __init__(self):
""" """
"""initialize plugin"""
self.form = None
def init(self):
"""
called when IDA is loading the plugin
"""
"""called when IDA is loading the plugin"""
logging.basicConfig(level=logging.INFO)
# check IDA version and database compatibility
@@ -51,18 +49,15 @@ class CapaExplorerPlugin(idaapi.plugin_t):
logger.debug("plugin initialized")
return idaapi.PLUGIN_KEEP
# plugin is good, but don't keep us in memory
return idaapi.PLUGIN_OK
def term(self):
"""
called when IDA is unloading the plugin
"""
"""called when IDA is unloading the plugin"""
logger.debug("plugin terminated")
def run(self, arg):
"""
called when IDA is running the plugin as a script
"""
"""called when IDA is running the plugin as a script"""
self.form = CapaExplorerForm(self.PLUGIN_NAME)
self.form.Show()
return True

View File

@@ -10,5 +10,8 @@ from capa.ida.plugin import CapaExplorerPlugin
def PLUGIN_ENTRY():
""" Mandatory entry point for IDAPython plugins """
"""mandatory entry point for IDAPython plugins
copy this script to your IDA plugins directory and start the plugin by navigating to Edit > Plugins in IDA Pro
"""
return CapaExplorerPlugin()

View File

@@ -31,8 +31,10 @@ settings = ida_settings.IDASettings("capa")
class CapaExplorerForm(idaapi.PluginForm):
"""form element for plugin interface"""
def __init__(self, name):
""" """
"""initialize form elements"""
super(CapaExplorerForm, self).__init__()
self.form_title = name
@@ -56,9 +58,11 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_menu_bar = None
def OnCreate(self, form):
""" """
"""called when plugin form is created"""
self.parent = self.FormToPyQtWidget(form)
self.parent.setWindowIcon(QICON)
# load interface elements
self.load_interface()
self.load_capa_results()
self.load_ida_hooks()
@@ -68,20 +72,23 @@ class CapaExplorerForm(idaapi.PluginForm):
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)
)
def OnClose(self, form):
""" form is closed """
"""called when form is closed
ensure any plugin modifications (e.g. hooks and UI changes) are reset before the plugin is closed
"""
self.unload_ida_hooks()
self.ida_reset()
logger.debug("form closed")
def load_interface(self):
""" load user interface """
"""load user interface"""
# load models
self.model_data = CapaExplorerDataModel()
@@ -113,17 +120,17 @@ class CapaExplorerForm(idaapi.PluginForm):
self.load_view_parent()
def load_view_tabs(self):
""" load tabs """
"""load tabs"""
tabs = QtWidgets.QTabWidget()
self.view_tabs = tabs
def load_view_menu_bar(self):
""" load menu bar """
"""load menu bar"""
bar = QtWidgets.QMenuBar()
self.view_menu_bar = bar
def load_view_attack(self):
""" load MITRE ATT&CK table """
"""load MITRE ATT&CK table"""
table_headers = [
"ATT&CK Tactic",
"ATT&CK Technique ",
@@ -145,7 +152,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_attack = table
def load_view_checkbox_limit_by(self):
""" load limit results by function checkbox """
"""load limit results by function checkbox"""
check = QtWidgets.QCheckBox("Limit results to current function")
check.setChecked(False)
check.stateChanged.connect(self.slot_checkbox_limit_by_changed)
@@ -153,7 +160,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_limit_results_by_function = check
def load_view_search_bar(self):
""" load the search bar control """
"""load the search bar control"""
line = QtWidgets.QLineEdit()
line.setPlaceholderText("search...")
line.textChanged.connect(self.search_model_proxy.set_query)
@@ -161,7 +168,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_search_bar = line
def load_view_parent(self):
""" load view parent """
"""load view parent"""
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.view_tabs)
@@ -170,7 +177,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.parent.setLayout(layout)
def load_view_tree_tab(self):
""" load capa tree tab view """
"""load tree view tab"""
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.view_limit_results_by_function)
layout.addWidget(self.view_search_bar)
@@ -182,7 +189,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_tabs.addTab(tab, "Tree View")
def load_view_attack_tab(self):
""" load MITRE ATT&CK tab view """
"""load MITRE ATT&CK view tab"""
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.view_attack)
@@ -192,6 +199,7 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_tabs.addTab(tab, "MITRE")
def load_file_menu(self):
"""load file menu controls"""
actions = (
("Rerun analysis", "Rerun capa analysis on current database", self.reload),
("Export results...", "Export capa results as JSON file", self.export_json),
@@ -199,15 +207,21 @@ class CapaExplorerForm(idaapi.PluginForm):
self.load_menu("File", actions)
def load_rules_menu(self):
"""load rules menu controls"""
actions = (("Change rules directory...", "Select new rules directory", self.change_rules_dir),)
self.load_menu("Rules", actions)
def load_view_menu(self):
"""load view menu controls"""
actions = (("Reset view", "Reset plugin view", self.reset),)
self.load_menu("View", actions)
def load_menu(self, title, actions):
""" load menu 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, _, handle) in actions:
action = QtWidgets.QAction(name, self.parent)
@@ -215,16 +229,18 @@ class CapaExplorerForm(idaapi.PluginForm):
menu.addAction(action)
def export_json(self):
""" export capa results as JSON file """
"""export capa results as JSON file"""
if not self.doc:
idaapi.info("No capa results to export.")
return
path = idaapi.ask_file(True, "*.json", "Choose file")
# user cancelled, entered blank input, etc.
if not path:
return
# check file exists, ask to override
if os.path.exists(path) and 1 != idaapi.ask_yn(1, "File already exists. Overwrite?"):
return
@@ -234,7 +250,9 @@ class CapaExplorerForm(idaapi.PluginForm):
)
def load_ida_hooks(self):
""" load IDA Pro UI hooks """
"""load IDA UI hooks"""
# map named action (defined in idagui.cfg) to Python function
action_hooks = {
"MakeName": self.ida_hook_rename,
"EditFunction": self.ida_hook_rename,
@@ -245,18 +263,20 @@ class CapaExplorerForm(idaapi.PluginForm):
self.ida_hooks.hook()
def unload_ida_hooks(self):
""" unload IDA Pro UI hooks """
"""unload IDA Pro UI hooks
must be called before plugin is completely destroyed
"""
if self.ida_hooks:
self.ida_hooks.unhook()
def ida_hook_rename(self, meta, post=False):
"""hook for IDA rename action
"""function hook for IDA "MakeName" and "EditFunction" actions
called twice, once before action and once after
action completes
called twice, once before action and once after action completes
@param meta: metadata cache
@param post: indicates pre or post action
@param meta: dict of key/value pairs set when action first called (may be empty)
@param post: False if action first call, True if action second call
"""
location = idaapi.get_screen_ea()
if not location or not capa.ida.helpers.is_func_start(location):
@@ -272,9 +292,10 @@ class CapaExplorerForm(idaapi.PluginForm):
meta["prev_name"] = curr_name
def ida_hook_screen_ea_changed(self, widget, new_ea, old_ea):
"""hook for IDA screen ea changed
"""function hook for IDA "screen ea changed" action
this hook is currently only relevant for limiting results displayed in the UI
called twice, once before action and once after action completes. this hook is currently only relevant
for limiting results displayed in the UI
@param widget: IDA widget type
@param new_ea: destination ea
@@ -296,20 +317,21 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_tree.resize_columns_to_content()
def ida_hook_rebase(self, meta, post=False):
"""hook for IDA rebase action
"""function hook for IDA "RebaseProgram" action
called twice, once before action and once after
action completes
called twice, once before action and once after action completes
@param meta: metadata cache
@param post: indicates pre or post action
@param meta: dict of key/value pairs set when action first called (may be empty)
@param post: False if action first call, True if action second call
"""
if post:
capa.ida.helpers.inform_user_ida_ui("Running capa analysis again after rebase")
self.reload()
def load_capa_results(self):
""" run capa analysis and render results in UI """
"""run capa analysis and render results in UI"""
# resolve rules directory - check self and settings first, then ask user
if not self.rule_path:
if "rule_path" in settings:
self.rule_path = settings["rule_path"]
@@ -370,6 +392,7 @@ class CapaExplorerForm(idaapi.PluginForm):
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()
@@ -378,11 +401,11 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.debug("render views completed.")
def set_view_tree_default_sort_order(self):
""" set capa tree view default sort order """
"""set tree view default sort order"""
self.view_tree.sortByColumn(CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION, QtCore.Qt.AscendingOrder)
def render_capa_doc_mitre_summary(self):
""" render capa MITRE ATT&CK results """
"""render MITRE ATT&CK results"""
tactics = collections.defaultdict(set)
for rule in rutils.capability_rules(self.doc):
@@ -419,29 +442,32 @@ class CapaExplorerForm(idaapi.PluginForm):
self.view_attack.setRowCount(max(len(column_one), len(column_two)))
for row, value in enumerate(column_one):
for (row, value) in enumerate(column_one):
self.view_attack.setItem(row, 0, self.render_new_table_header_item(value))
for row, value in enumerate(column_two):
for (row, value) in enumerate(column_two):
self.view_attack.setItem(row, 1, QtWidgets.QTableWidgetItem(value))
# resize columns to content
self.view_attack.resizeColumnsToContents()
def render_new_table_header_item(self, text):
""" create new table header item with default style """
"""create new table header item with our style
@param text: header text to display
"""
item = QtWidgets.QTableWidgetItem(text)
item.setForeground(QtGui.QColor(88, 139, 174))
font = QtGui.QFont()
font.setBold(True)
item.setFont(font)
return item
def ida_reset(self):
""" reset IDA UI """
"""reset plugin UI
called when user selects plugin reset from menu
"""
self.model_data.reset()
self.view_tree.reset()
self.view_limit_results_by_function.setChecked(False)
@@ -449,7 +475,10 @@ class CapaExplorerForm(idaapi.PluginForm):
self.set_view_tree_default_sort_order()
def reload(self):
""" reload views and re-run capa analysis """
"""re-run capa analysis and reload UI controls
called when user selects plugin reload from menu
"""
self.ida_reset()
self.range_model_proxy.invalidate()
self.search_model_proxy.invalidate()
@@ -483,8 +512,9 @@ class CapaExplorerForm(idaapi.PluginForm):
def slot_checkbox_limit_by_changed(self, state):
"""slot activated if checkbox clicked
if checked, configure function filter if screen location is located
in function, otherwise clear filter
if checked, configure function filter if screen location is located in function, otherwise clear filter
@param state: checked state
"""
if state == QtCore.Qt.Checked:
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
@@ -496,20 +526,26 @@ class CapaExplorerForm(idaapi.PluginForm):
def limit_results_to_function(self, f):
"""add filter to limit results to current function
adds new address range filter to include function bounds, allowing basic blocks matched within a function
to be included in the results
@param f: (IDA func_t)
"""
if f:
self.range_model_proxy.add_address_range_filter(f.start_ea, f.end_ea)
else:
# if function not exists don't display any results (address should not be -1)
# if function not exists don't display any results (assume address never -1)
self.range_model_proxy.add_address_range_filter(-1, -1)
def ask_user_directory(self):
""" create Qt dialog to ask user for a directory """
"""create Qt dialog to ask user for a directory"""
return str(QtWidgets.QFileDialog.getExistingDirectory(self.parent, "Select rules directory", self.rule_path))
def change_rules_dir(self):
""" allow user to change rules directory """
"""allow user to change rules directory
user selection stored in settings for future runs
"""
rule_path = self.ask_user_directory()
if not rule_path:
logger.warning("no rules directory selected. nothing to do.")

View File

@@ -39,7 +39,7 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
return 0
def postprocess_action(self):
""" called after action completed """
"""called after action completed"""
if not self.process_action_handle:
return
@@ -55,6 +55,6 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea)
def reset(self):
""" reset internal state """
"""reset internal state"""
self.process_action_handle = None
self.process_action_meta.clear()

View File

@@ -28,20 +28,21 @@ def info_to_name(display):
def location_to_hex(location):
""" convert location to hex for display """
"""convert location to hex for display"""
return "%08X" % location
class CapaExplorerDataItem(object):
""" store data for CapaExplorerDataModel """
"""store data for CapaExplorerDataModel"""
def __init__(self, parent, data):
""" """
"""initialize item"""
self.pred = parent
self._data = data
self.children = []
self._checked = False
# default state for item
self.flags = (
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
@@ -53,117 +54,146 @@ class CapaExplorerDataItem(object):
self.pred.appendChild(self)
def setIsEditable(self, isEditable=False):
""" modify item flags to be editable or not """
"""modify item editable flags
@param isEditable: True, can edit, False cannot edit
"""
if isEditable:
self.flags |= QtCore.Qt.ItemIsEditable
else:
self.flags &= ~QtCore.Qt.ItemIsEditable
def setChecked(self, checked):
""" set item as checked """
"""set item as checked
@param checked: True, item checked, False item not checked
"""
self._checked = checked
def isChecked(self):
""" get item is checked """
"""get item is checked"""
return self._checked
def appendChild(self, item):
"""add child item
"""add a new child to specified item
@param item: CapaExplorerDataItem*
@param item: CapaExplorerDataItem
"""
self.children.append(item)
def child(self, row):
"""get child row
@param row: TODO
@param row: row number
"""
return self.children[row]
def childCount(self):
""" get child count """
"""get child count"""
return len(self.children)
def columnCount(self):
""" get column count """
"""get column count"""
return len(self._data)
def data(self, column):
""" get data at column """
"""get data at column
@param: column number
"""
try:
return self._data[column]
except IndexError:
return None
def parent(self):
""" get parent """
"""get parent"""
return self.pred
def row(self):
""" get row location """
"""get row location"""
if self.pred:
return self.pred.children.index(self)
return 0
def setData(self, column, value):
""" set data in column """
"""set data in column
@param column: column number
@value: value to set (assume str)
"""
self._data[column] = value
def children(self):
""" yield children """
"""yield children"""
for child in self.children:
yield child
def removeChildren(self):
""" remove children from node """
"""remove children"""
del self.children[:]
def __str__(self):
""" get string representation of columns """
"""get string representation of columns
used for copy-n-paste operations
"""
return " ".join([data for data in self._data if data])
@property
def info(self):
""" return data stored in information column """
"""return data stored in information column"""
return self._data[0]
@property
def location(self):
""" return data stored in location column """
"""return data stored in location column"""
try:
# address stored as str, convert to int before return
return int(self._data[1], 16)
except ValueError:
return None
@property
def details(self):
""" return data stored in details column """
"""return data stored in details column"""
return self._data[2]
class CapaExplorerRuleItem(CapaExplorerDataItem):
""" store data relevant to capa function result """
"""store data for rule result"""
fmt = "%s (%d matches)"
def __init__(self, parent, name, namespace, count, source):
""" """
"""initialize item
@param parent: parent node
@param name: rule name
@param namespace: rule namespace
@param count: number of match for this rule
@param source: rule source (tooltip)
"""
display = self.fmt % (name, count) if count > 1 else name
super(CapaExplorerRuleItem, self).__init__(parent, [display, "", namespace])
self._source = source
@property
def source(self):
""" return rule contents for display """
"""return rule source to display (tooltip)"""
return self._source
class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
""" store data relevant to capa function match result """
"""store data for rule match"""
def __init__(self, parent, display, source=""):
""" """
"""initialize item
@param parent: parent node
@param display: text to display in UI
@param source: rule match source to display (tooltip)
"""
super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, "", ""])
self._source = source
@@ -174,82 +204,125 @@ class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
class CapaExplorerFunctionItem(CapaExplorerDataItem):
""" store data relevant to capa function result """
"""store data for function match"""
fmt = "function(%s)"
def __init__(self, parent, location):
""" """
"""initialize item
@param parent: parent node
@param location: virtual address of function as seen by IDA
"""
super(CapaExplorerFunctionItem, self).__init__(
parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""]
)
@property
def info(self):
""" """
"""return function name"""
info = super(CapaExplorerFunctionItem, self).info
display = info_to_name(info)
return display if display else info
@info.setter
def info(self, display):
""" """
"""set function name
called when user changes function name in plugin UI
@param display: new function name to display
"""
self._data[0] = self.fmt % display
class CapaExplorerSubscopeItem(CapaExplorerDataItem):
""" store data relevant to subscope """
"""store data for subscope match"""
fmt = "subscope(%s)"
def __init__(self, parent, scope):
""" """
"""initialize item
@param parent: parent node
@param scope: subscope name
"""
super(CapaExplorerSubscopeItem, self).__init__(parent, [self.fmt % scope, "", ""])
class CapaExplorerBlockItem(CapaExplorerDataItem):
""" store data relevant to capa basic block result """
"""store data for basic block match"""
fmt = "basic block(loc_%08X)"
def __init__(self, parent, location):
""" """
"""initialize item
@param parent: parent node
@param location: virtual address of basic block as seen by IDA
"""
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ""])
class CapaExplorerDefaultItem(CapaExplorerDataItem):
""" store data relevant to capa default result """
"""store data for default match e.g. statement (and, or)"""
def __init__(self, parent, display, details="", location=None):
""" """
"""initialize item
@param parent: parent node
@param display: text to display in UI
@param details: text to display in details section of UI
@param location: virtual address as seen by IDA
"""
location = location_to_hex(location) if location else ""
super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
class CapaExplorerFeatureItem(CapaExplorerDataItem):
""" store data relevant to capa feature result """
"""store data for feature match"""
def __init__(self, parent, display, location="", details=""):
""" """
"""initialize item
@param parent: parent node
@param display: text to display in UI
@param details: text to display in details section of UI
@param location: virtual address as seen by IDA
"""
location = location_to_hex(location) if location else ""
super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
""" store data relevant to an instruction preview """
"""store data for instruction match"""
def __init__(self, parent, display, location):
""" """
"""initialize item
details section shows disassembly view for match
@param parent: parent node
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
details = capa.ida.helpers.get_disasm_line(location)
super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
""" store data relevant to byte preview """
"""store data for byte match"""
def __init__(self, parent, display, location):
""" """
"""initialize item
details section shows byte preview for match
@param parent: parent node
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
byte_snap = idaapi.get_bytes(location, 32)
if byte_snap:
@@ -266,9 +339,14 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
""" store data relevant to string preview """
"""store data for string match"""
def __init__(self, parent, display, location):
""" """
"""initialize item
@param parent: parent node
@param display: text to display in UI
@param location: virtual address as seen by IDA
"""
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)

View File

@@ -33,7 +33,7 @@ DEFAULT_HIGHLIGHT = 0xD096FF
class CapaExplorerDataModel(QtCore.QAbstractItemModel):
""" """
"""model for displaying hierarchical results return by capa"""
COLUMN_INDEX_RULE_INFORMATION = 0
COLUMN_INDEX_VIRTUAL_ADDRESS = 1
@@ -42,14 +42,16 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
COLUMN_COUNT = 3
def __init__(self, parent=None):
""" """
"""initialize model"""
super(CapaExplorerDataModel, self).__init__(parent)
# root node does not have parent, contains header columns
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
def reset(self):
""" """
# reset checkboxes and color highlights
# TODO: make less hacky
"""reset UI elements (e.g. checkboxes, IDA color highlights)
called when view wants to reset UI display
"""
for idx in range(self.root_node.childCount()):
root_index = self.index(idx, 0, QtCore.QModelIndex())
for model_index in self.iterateChildrenIndexFromRootIndex(root_index, ignore_root=False):
@@ -58,15 +60,18 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
self.dataChanged.emit(model_index, model_index)
def clear(self):
""" """
"""clear model data
called when view wants to clear UI display
"""
self.beginResetModel()
self.root_node.removeChildren()
self.endResetModel()
def columnCount(self, model_index):
"""get the number of columns for the children of the given parent
"""return number of columns for the children of the given parent
@param model_index: QModelIndex*
@param model_index: QModelIndex
@retval column count
"""
@@ -76,9 +81,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return self.root_node.columnCount()
def data(self, model_index, role):
"""get data stored under the given role for the item referred to by the index
"""return data stored at given index by display role
@param model_index: QModelIndex*
this function is used to control UI elements (e.g. text font, color, etc.) based on column, item type, etc.
@param model_index: QModelIndex
@param role: QtCore.Qt.*
@retval data to be displayed
@@ -150,9 +157,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return None
def flags(self, model_index):
"""get item flags for given index
"""return item flags for given index
@param model_index: QModelIndex*
@param model_index: QModelIndex
@retval QtCore.Qt.ItemFlags
"""
@@ -162,13 +169,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return model_index.internalPointer().flags
def headerData(self, section, orientation, role):
"""get data for the given role and section in the header with the specified orientation
"""return data for the given role and section in the header with the specified orientation
@param section: int
@param orientation: QtCore.Qt.Orientation
@param role: QtCore.Qt.DisplayRole
@retval header data list()
@retval header data
"""
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.root_node.data(section)
@@ -176,13 +183,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return None
def index(self, row, column, parent):
"""get index of the item in the model specified by the given row, column and parent index
"""return index of the item by row, column, and parent index
@param row: int
@param column: int
@param parent: QModelIndex*
@param row: item row
@param column: item column
@param parent: QModelIndex of parent
@retval QModelIndex*
@retval QModelIndex of item
"""
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
@@ -200,13 +207,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return QtCore.QModelIndex()
def parent(self, model_index):
"""get parent of the model item with the given index
"""return parent index by child index
if the item has no parent, an invalid QModelIndex* is returned
if the item has no parent, an invalid QModelIndex is returned
@param model_index: QModelIndex*
@param model_index: QModelIndex of child
@retval QModelIndex*
@retval QModelIndex of parent
"""
if not model_index.isValid():
return QtCore.QModelIndex()
@@ -222,10 +229,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True):
"""depth-first traversal of child nodes
@param model_index: QModelIndex*
@param ignore_root: if set, do not return root index
@param model_index: QModelIndex of starting item
@param ignore_root: True, do not yield root index, False yield root index
@retval yield QModelIndex*
@retval yield QModelIndex
"""
visited = set()
stack = deque((model_index,))
@@ -247,10 +254,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
stack.append(child_index.child(idx, 0))
def reset_ida_highlighting(self, item, checked):
"""reset IDA highlight for an item
"""reset IDA highlight for item
@param item: capa explorer item
@param checked: indicates item is or not checked
@param item: CapaExplorerDataItem
@param checked: True, item checked, False item not checked
"""
if not isinstance(
item, (CapaExplorerStringViewItem, CapaExplorerInstructionViewItem, CapaExplorerByteViewItem)
@@ -274,13 +281,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
idc.set_color(item.location, idc.CIC_ITEM, item.ida_highlight)
def setData(self, model_index, value, role):
"""set the role data for the item at index to value
"""set data at index by role
@param model_index: QModelIndex*
@param value: QVariant*
@param model_index: QModelIndex of item
@param value: value to set
@param role: QtCore.Qt.EditRole
@retval True/False
"""
if not model_index.isValid():
return False
@@ -315,12 +320,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
return False
def rowCount(self, model_index):
"""get the number of rows under the given parent
"""return number of rows under item by index
when the parent is valid it means that is returning the number of
children of parent
when the parent is valid it means that is returning the number of children of parent
@param model_index: QModelIndex*
@param model_index: QModelIndex
@retval row count
"""
@@ -340,11 +344,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param parent: parent to which new child is assigned
@param statement: statement read from doc
@param locations: locations of children (applies to range only?)
@param doc: capa result doc
"statement": {
"type": "or"
},
@param doc: result doc
"""
if statement["type"] in ("and", "or", "optional"):
display = statement["type"]
@@ -398,24 +398,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param parent: parent node to which new child is assigned
@param match: match read from doc
@param doc: capa result doc
"matches": {
"0": {
"children": [],
"locations": [
4317184
],
"node": {
"feature": {
"section": ".rsrc",
"type": "section"
},
"type": "feature"
},
"success": true
}
},
@param doc: result doc
"""
if not match["success"]:
# TODO: display failed branches at some point? Help with debugging rules?
@@ -475,15 +458,6 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
"""convert capa doc feature type string to display string for ui
@param feature: capa feature read from doc
Example:
"feature": {
"bytes": "01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46",
"description": "CLSID_ShellLink",
"type": "bytes"
}
bytes(01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_ShellLink)
"""
if feature[feature["type"]]:
if feature.get("description", ""):
@@ -500,13 +474,6 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param feature: capa doc feature node
@param locations: locations identified for feature
@param doc: capa doc
Example:
"feature": {
"description": "FILE_WRITE_DATA",
"number": "0x2",
"type": "number"
}
"""
display = self.capa_doc_feature_to_display(feature)
@@ -535,14 +502,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
@param feature: feature read from doc
@param doc: capa feature doc
@param location: address of feature
@param display: text to display in plugin ui
Example:
"feature": {
"description": "FILE_WRITE_DATA",
"number": "0x2",
"type": "number"
}
@param display: text to display in plugin UI
"""
# special handling for characteristic pending type
if feature["type"] == "characteristic":
@@ -598,7 +558,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
def update_function_name(self, old_name, new_name):
"""update all instances of old function name with new function name
@param old_name: previous function name
called when user updates function name using plugin UI
@param old_name: old function name
@param new_name: new function name
"""
# create empty root index for search

View File

@@ -13,20 +13,25 @@ from capa.ida.plugin.model import CapaExplorerDataModel
class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
""" """
super(CapaExplorerRangeProxyModel, self).__init__(parent)
"""filter results based on virtual address range as seen by IDA
implements filtering for "limit results by current function" checkbox in plugin UI
minimum and maximum virtual addresses are used to filter results to a specific address range. this allows
basic blocks to be included when limiting results to a specific function
"""
def __init__(self, parent=None):
"""initialize proxy filter"""
super(CapaExplorerRangeProxyModel, self).__init__(parent)
self.min_ea = None
self.max_ea = None
def lessThan(self, left, right):
"""true if the value of the left item is less than value of right item
"""return True if left item is less than right item, else False
@param left: QModelIndex*
@param right: QModelIndex*
@retval True/False
@param left: QModelIndex of left
@param right: QModelIndex of right
"""
ldata = left.internalPointer().data(left.column())
rdata = right.internalPointer().data(right.column())
@@ -44,13 +49,11 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
return ldata.lower() < rdata.lower()
def filterAcceptsRow(self, row, parent):
"""true if the item in the row indicated by the given row and parent
should be included in the model; otherwise returns false
"""return true if the item in the row indicated by the given row and parent should be included in the model;
otherwise return false
@param row: int
@param parent: QModelIndex*
@retval True/False
@param row: row number
@param parent: QModelIndex of parent
"""
if self.filter_accepts_row_self(row, parent):
return True
@@ -67,7 +70,11 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
return False
def index_has_accepted_children(self, row, parent):
""" """
"""return True if parent has one or more children that match filter, else False
@param row: row number
@param parent: QModelIndex of parent
"""
model_index = self.sourceModel().index(row, 0, parent)
if model_index.isValid():
@@ -80,7 +87,11 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
return False
def filter_accepts_row_self(self, row, parent):
""" """
"""return True if filter accepts row, else False
@param row: row number
@param parent: QModelIndex of parent
"""
# filter not set
if self.min_ea is None and self.max_ea is None:
return True
@@ -88,9 +99,11 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
index = self.sourceModel().index(row, 0, parent)
data = index.internalPointer().data(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS)
# virtual address may be empty
if not data:
return False
# convert virtual address str to int
ea = int(data, 16)
if self.min_ea <= ea and ea < self.max_ea:
@@ -99,7 +112,13 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
return False
def add_address_range_filter(self, min_ea, max_ea):
""" """
"""add new address range filter
called when user checks "limit results by current function" in plugin UI
@param min_ea: minimum virtual address as seen by IDA
@param max_ea: maximum virtual address as seen by IDA
"""
self.min_ea = min_ea
self.max_ea = max_ea
@@ -107,7 +126,10 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
self.invalidateFilter()
def reset_address_range_filter(self):
""" """
"""remove address range filter (accept all results)
called when user un-checks "limit results by current function" in plugin UI
"""
self.min_ea = None
self.max_ea = None
self.invalidateFilter()

View File

@@ -14,17 +14,16 @@ from capa.ida.plugin.model import CapaExplorerDataModel
class CapaExplorerQtreeView(QtWidgets.QTreeView):
"""capa explorer QTreeView implementation
"""tree view used to display hierarchical capa results
view controls UI action responses and displays data from
CapaExplorerDataModel
view controls UI action responses and displays data from CapaExplorerDataModel
view does not modify CapaExplorerDataModel directly - data
modifications should be implemented in CapaExplorerDataModel
view does not modify CapaExplorerDataModel directly - data modifications should be implemented
in CapaExplorerDataModel
"""
def __init__(self, model, parent=None):
""" initialize CapaExplorerQTreeView """
"""initialize view"""
super(CapaExplorerQtreeView, self).__init__(parent)
self.setModel(model)
@@ -55,22 +54,21 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def reset(self):
"""reset user interface changes
called when view should reset any user interface changes
made since the last reset e.g. IDA window highlighting
called when view should reset any user interface changes made since the last reset e.g. IDA window highlighting
"""
self.expandToDepth(0)
self.resize_columns_to_content()
def resize_columns_to_content(self):
""" reset view columns to contents """
"""reset view columns to contents"""
self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents)
def map_index_to_source_item(self, model_index):
"""map proxy model index to source model item
@param model_index: QModelIndex*
@param model_index: QModelIndex
@retval QObject*
@retval QObject
"""
# assume that self.model here is either:
# - CapaExplorerDataModel, or
@@ -107,7 +105,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
@param data: data passed to slot
@param slot: slot to connect
@retval QAction*
@retval QAction
"""
action = QtWidgets.QAction(display, self.parent)
action.setData(data)
@@ -120,7 +118,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
@param data: tuple
@yield QAction*
@yield QAction
"""
default_actions = (
("Copy column", data, self.slot_copy_column),
@@ -136,7 +134,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
@param data: tuple
@yield QAction*
@yield QAction
"""
function_actions = (("Rename function", data, self.slot_rename_function),)
@@ -153,11 +151,11 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
creates custom context menu containing default actions
@param pos: TODO
@param item: TODO
@param model_index: TODO
@param pos: cursor position
@param item: CapaExplorerDataItem
@param model_index: QModelIndex
@retval QMenu*
@retval QMenu
"""
menu = QtWidgets.QMenu()
@@ -169,14 +167,13 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def load_function_item_context_menu(self, pos, item, model_index):
"""create function custom context menu
creates custom context menu containing actions specific to functions
and the default actions
creates custom context menu with both default actions and function actions
@param pos: TODO
@param item: TODO
@param model_index: TODO
@param pos: cursor position
@param item: CapaExplorerDataItem
@param model_index: QModelIndex
@retval QMenu*
@retval QMenu
"""
menu = QtWidgets.QMenu()
@@ -188,8 +185,8 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def show_custom_context_menu(self, menu, pos):
"""display custom context menu in view
@param menu: TODO
@param pos: TODO
@param menu: QMenu to display
@param pos: cursor position
"""
if menu:
menu.exec_(self.viewport().mapToGlobal(pos))
@@ -197,10 +194,9 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def slot_copy_column(self, action):
"""slot connected to custom context menu
allows user to select a column and copy the data
to clipboard
allows user to select a column and copy the data to clipboard
@param action: QAction*
@param action: QAction
"""
_, item, model_index = action.data()
self.send_data_to_clipboard(item.data(model_index.column()))
@@ -208,10 +204,9 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def slot_copy_row(self, action):
"""slot connected to custom context menu
allows user to select a row and copy the space-delimited
data to clipboard
allows user to select a row and copy the space-delimited data to clipboard
@param action: QAction*
@param action: QAction
"""
_, item, _ = action.data()
self.send_data_to_clipboard(str(item))
@@ -219,10 +214,9 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def slot_rename_function(self, action):
"""slot connected to custom context menu
allows user to select a edit a function name and push
changes to IDA
allows user to select a edit a function name and push changes to IDA
@param action: QAction*
@param action: QAction
"""
_, item, model_index = action.data()
@@ -234,10 +228,9 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
def slot_custom_context_menu_requested(self, pos):
"""slot connected to custom context menu request
displays custom context menu to user containing action
relevant to the data item selected
displays custom context menu to user containing action relevant to the item selected
@param pos: TODO
@param pos: cursor position
"""
model_index = self.indexAt(pos)
item = self.map_index_to_source_item(model_index)
@@ -256,9 +249,11 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
self.show_custom_context_menu(menu, pos)
def slot_double_click(self, model_index):
"""slot connected to double click event
"""slot connected to double-click event
@param model_index: QModelIndex*
if address column clicked, navigate IDA to address, else un/expand item clicked
@param model_index: QModelIndex
"""
if not model_index.isValid():
return