def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) tool_menu = QMenu() tool_menu.addSeparator() tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addSeparator() tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addAction(QAction("Default empty group", self)) tool_menu.addSeparator() tool_menu.addAction(QAction("Default empty group", self)) tool_menu.setToolTipsVisible(True) self.ui.toolButton.setMenu(tool_menu) self.ui.toolButton.clicked.connect(self.on_toolButton)
class Window(QMainWindow): def __init__(self): super(Window, self).__init__() # Config -------------------------------------- self.objectList = {} self.systemIcon = { 'folder': QIcon(QApplication.style().standardIcon(QStyle.SP_DirIcon)), 'folderLink': QIcon(QApplication.style().standardIcon(QStyle.SP_DirLinkIcon)), 'file': QIcon(QApplication.style().standardIcon(QStyle.SP_FileIcon)), 'quit': QIcon(QApplication.style().standardIcon( QStyle.SP_DialogCloseButton)) } self.filetypeIcon = Icons.icon_filetypes_flat(self) self.includeExclude = IncludeExclude() self.includeExclude.includePaths = ['Examples'] self.includeExclude.includeExtensions = ['md', 'py', 'png'] self.includeExclude.excludeFilenames = ['gitignore'] self.includeExclude.excludePaths = ['git', '__pycache__'] # -------------------------------------------- self.create_menu() self.add_directory( '&Scripts', '/media/m/Data/Development/Phyton/TreeViewUltraMenu/Python-UltraMenu' ) self.add_exit() def create_menu(self): self.menu = QMenu(self) # Enable theme, uncomment: self.menu.setStyleSheet(appStyle) self.menu.setCursor(QtCore.Qt.PointingHandCursor) tt1 = self.menu.addAction('Tooltip 1') tt1.setToolTip('t1') tt2 = self.menu.addAction('Tooltip 2') tt2.setToolTip('t2') tt3 = self.menu.addAction('Tooltip 3') tt3.setWhatsThis('setWhatsThis') tt3.setIconText('q') tt3.iconText() # custom style label text = QLabel("Label with custom text", self) text.setProperty('class', 'singlestyle') styleItem = QWidgetAction(self) styleItem.setDefaultWidget(text) styleItem.setProperty('class', 'singlestyle') tt4 = self.menu.addAction(styleItem) # tt4.setProperty('class', 'singlestyle') # tt4.Priority # tt4.setStyleSheet("QLabel { color: rgb(50, 50, 50); font-size: 11px; background-color: rgba(188, 188, 188, 50); border: 1px solid rgba(188, 188, 188, 250); }") self.menu.setToolTipsVisible(True) # self.menu.setIcon # self.menu.setwha(True) # self.menu.hovered.connect(lambda tt1= tt1, tt1()) # self.menu.keyPressEvent(self.keyPressEvent) # pos = tt3. self.menu.installEventFilter(self) # widgetRect = self.menu.mapToGlobal() p = (0, 0) # print(self.menu.mapToGlobal(0,0)) widgetRect = self.geometry() print(widgetRect) # tt2.hovered.connect(lambda tt2=self.tt2, tt2.tooltip()) tt3.hovered.connect(lambda pos=[self], parent=self.menu, index=2: self. show_toolTip(pos, parent, index)) # Listen for All KeyPress/Mouse events def eventFilter(self, widget, event): if (event.type() == QtCore.QEvent.KeyRelease and widget is self.menu): self.menu.toolTip() print('all') # self. key = event.key() if key == QtCore.Qt.Key_Escape: print('escape') else: if key == QtCore.Qt.Key_Return: print('escape') elif key == QtCore.Qt.Key_Enter: print('escape') elif key == QtCore.Qt.Key_C: print('Key - c') return True # return QtGui.QWidget.eventFilter(self, widget, event) def show_toolTip(self, pos, parent, index): ''' **Parameters** pos: list list of all parent widget up to the upmost parent: PySide.QtGui.QAction the parent QAction index: int place within the QMenu, beginning with zero ''' position_x = 0 position_y = 0 for widget in pos: position_x += widget.pos().x() position_y += widget.pos().y() point = QtCore.QPoint() point.setX(position_x) point.setY(position_y + index * 22) # set y Position of QToolTip QToolTip.showText(point, parent.toolTip()) def add_directory(self, label, dir): qtParentMenuItem = self.menu.addMenu(self.systemIcon['folder'], label) parentMenuClass = FolderItem(qtMenuItem=qtParentMenuItem, label=label, iconPath='', globalHotkey='', path=Path(dir)) uid = parentMenuClass.getUid() self.objectList[uid] = parentMenuClass self.add_directory_submenu(parentMenuClass) def add_directory_submenu(self, parentMenuClass: FolderItem): qtParentMenuItem = parentMenuClass.getQtMenuItem() # Don't add the menu items multiple times, every time you hover a submenu. if parentMenuClass.isSubmenuItemsAdded == True: return None parentMenuClass.isSubmenuItemsAdded = True dirPath: str = parentMenuClass.getFullPath() osPaths = sorted(Path(dirPath).glob('*')) osPaths.sort(key=lambda x: x.is_file()) filesAndFoldersArr = [] for item in osPaths: # path = dirPath + '/' + item.name if item.is_dir( ) == True and self.includeExclude.folderIsNotExcluded(item): folderIcon = self.systemIcon['folder'] if item.is_symlink(): folderIcon = self.systemIcon['folderLink'] qtFolderItem = qtParentMenuItem.addMenu( folderIcon, '&' + item.name) folderItemClass = FolderItem(qtMenuItem=qtFolderItem, label=item.name, iconPath='', globalHotkey='', path=item) uid = folderItemClass.getUid() self.objectList[uid] = folderItemClass filesAndFoldersArr.append(folderItemClass) qtFolderItem.aboutToShow.connect( lambda folderItemClass=folderItemClass: self. add_directory_submenu(folderItemClass)) # newFolder.triggered.connect(self.action_directory(item.uid)) elif item.is_file() and self.includeExclude.fileIsNotExcluded( item): fileExt = item.suffix qtIcon = self.systemIcon['file'] # If we have a icon for the filetype, use that instead if fileExt in self.filetypeIcon: qtIcon = QIcon(str(self.filetypeIcon[fileExt])) qtFileItem = qtParentMenuItem.addAction(qtIcon, item.name) folderItemClass = FolderItem(qtMenuItem=qtFileItem, label=item.name, iconPath='', globalHotkey='', path=item) uid = folderItemClass.getUid() self.objectList[uid] = folderItemClass filesAndFoldersArr.append(folderItemClass) # filename # item.name # extension # item.suffix # newFile.triggered.connect(self.action_directory(item.uid)) # # newFile.hovered.connect(self.exit_app) # # newFile.hovered.connect(lambda: item.printUid()) # # func = self.hover() # # newFile.hovered.connect(lambda f=func,arg=newFile:f(arg)) else: print("It is a special file (socket, FIFO, device file)") def action_directory(self, uid): print(uid) def add_exit(self): exit = self.menu.addAction(self.systemIcon['quit'], '&Quit') self.menu.insertSeparator(exit) exit.triggered.connect(self.exit_app) self.menu.exec_(QCursor.pos()) def exit_app(self): self.close() sys.exit(0)
class MyDockWidget(cutter.CutterDockWidget): def __init__(self, parent, action): super(MyDockWidget, self).__init__(parent, action) self.setObjectName("Capa explorer") self.setWindowTitle("Capa explorer") self._config = CutterBindings.Configuration.instance() self.model_data = CapaExplorerDataModel() self.range_model_proxy = CapaExplorerRangeProxyModel() self.range_model_proxy.setSourceModel(self.model_data) self.search_model_proxy = CapaExplorerSearchProxyModel() self.search_model_proxy.setSourceModel(self.range_model_proxy) self.create_view_tabs() self.create_menu() self.create_tree_tab_ui() self.create_view_attack() self.connect_signals() self.setWidget(self.tabs) self.show() def create_view_tabs(self): # Create tabs container self.tabs = QTabWidget() # Create the tabs self.tab_attack = QWidget(self.tabs) self.tab_tree_w_model = QWidget(self.tabs) self.tabs.addTab(self.tab_tree_w_model, "Tree View") self.tabs.addTab(self.tab_attack, "MITRE") def create_menu(self): # Define menu actions # Text, tooltip, function, enabled before file load self.disabled_menu_items = [] menu_actions = [ ("Load JSON file", '', self.cma_load_file, True), (), ("Auto rename functions", 'Auto renames functions according to capa detections, can result in very long function names.', self.cma_analyze_and_rename, False), ("Create flags", 'Creates flagspaces and flags from capa detections.', self.cma_create_flags, False), (), ("About", '', self.cma_display_about, True), ] self.capa_menu = QMenu() self.capa_menu.setToolTipsVisible(True) # Create qactions for action in menu_actions: if not len(action): # Create separator on empty self.capa_menu.addSeparator() continue a = QAction(self) a.setText(action[0]) a.setToolTip(action[1]) a.triggered.connect(action[2]) a.setEnabled(action[3]) if not action[3]: self.disabled_menu_items.append(a) self.capa_menu.addAction(a) # Create menu button font = QFont() font.setBold(True) self.btn_menu = QToolButton() self.btn_menu.setText('...') self.btn_menu.setFont(font) self.btn_menu.setPopupMode(QToolButton.InstantPopup) self.btn_menu.setMenu(self.capa_menu) self.btn_menu.setStyleSheet( 'QToolButton::menu-indicator { image: none; }') self.tabs.setCornerWidget(self.btn_menu, corner=Qt.TopRightCorner) def create_tree_tab_ui(self): self.capa_tree_view_layout = QVBoxLayout() self.capa_tree_view_layout.setAlignment(Qt.AlignTop) self.chk_fcn_scope = QCheckBox("Limit to Current function") #TODO: reset state on load file self.chk_fcn_scope.setChecked(False) self.chk_fcn_scope.stateChanged.connect( self.slot_checkbox_limit_by_changed) self.input_search = QLineEdit() self.input_search.setStyleSheet("margin:0px; padding:0px;") self.input_search.setPlaceholderText("search...") self.input_search.textChanged.connect( self.slot_limit_results_to_search) self.filter_controls_container = QGroupBox() self.filter_controls_container.setObjectName("scope") self.filter_controls_container.setFlat(True) self.filter_controls_container.setStyleSheet( "#scope{border:0px; padding:0px; margin:0px;subcontrol-origin: padding; subcontrol-position: left top;}" ) self.filter_controls_layout = QHBoxLayout( self.filter_controls_container) self.filter_controls_layout.setContentsMargins(0, 0, 0, 0) self.filter_controls_layout.addWidget(self.input_search) self.filter_controls_layout.addWidget(self.chk_fcn_scope) self.view_tree = CapaExplorerQtreeView(self.search_model_proxy) self.view_tree.setModel(self.search_model_proxy) # Make it look a little nicer when no results are loaded self.view_tree.header().setStretchLastSection(True) self.capa_tree_view_layout.addWidget(self.filter_controls_container) self.capa_tree_view_layout.addWidget(self.view_tree) self.tab_tree_w_model.setLayout(self.capa_tree_view_layout) def create_view_attack(self): table_headers = [ "ATT&CK Tactic", "ATT&CK Technique ", ] table = QTableWidget() table.setColumnCount(len(table_headers)) table.verticalHeader().setVisible(False) table.setSortingEnabled(False) table.setEditTriggers(QAbstractItemView.NoEditTriggers) table.setFocusPolicy(Qt.NoFocus) table.setSelectionMode(QAbstractItemView.NoSelection) table.setHorizontalHeaderLabels(table_headers) table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) table.horizontalHeader().setStretchLastSection(True) table.setShowGrid(False) table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeToContents) table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) #table.setStyleSheet("QTableWidget::item { padding: 25px; }") attack_view_layout = QVBoxLayout() attack_view_layout.setAlignment(Qt.AlignTop) self.attack_table = table attack_view_layout.addWidget(self.attack_table) self.tab_attack.setLayout(attack_view_layout) return table def connect_signals(self): QObject.connect(cutter.core(), SIGNAL("functionRenamed(RVA, QString)"), self.model_data.refresh_function_names) QObject.connect(cutter.core(), SIGNAL("functionsChanged()"), self.model_data.refresh_function_names) QObject.connect(cutter.core(), SIGNAL("seekChanged(RVA)"), self.signal_shim_slot_checkbox_limit_by_changed) def render_new_table_header_item(self, text): """create new table header item with our style @param text: header text to display """ item = QTableWidgetItem(text) item.setForeground(self._config.getColor("graph.true")) font = QFont() font.setBold(True) item.setFont(font) return item def fill_attack_table(self, rules): tactics = collections.defaultdict(set) for key, rule in rules.items(): if not rule["meta"].get("att&ck"): continue for attack in rule["meta"]["att&ck"]: tactic, _, rest = attack.partition("::") if "::" in rest: technique, _, rest = rest.partition("::") subtechnique, _, id = rest.rpartition(" ") tactics[tactic].add((technique, subtechnique, id)) else: technique, _, id = rest.rpartition(" ") tactics[tactic].add((technique, id)) column_one = [] column_two = [] for (tactic, techniques) in sorted(tactics.items()): column_one.append(tactic.upper()) # add extra space when more than one technique column_one.extend(["" for i in range(len(techniques) - 1)]) for spec in sorted(techniques): if len(spec) == 2: technique, id = spec column_two.append("%s %s" % (technique, id)) elif len(spec) == 3: technique, subtechnique, id = spec column_two.append("%s::%s %s" % (technique, subtechnique, id)) else: raise RuntimeError("unexpected ATT&CK spec format") self.attack_table.setRowCount(max(len(column_one), len(column_two))) for (row, value) in enumerate(column_one): self.attack_table.setItem(row, 0, self.render_new_table_header_item(value)) for (row, value) in enumerate(column_two): self.attack_table.setItem(row, 1, QTableWidgetItem(value)) def enable_menu_items_after_load(self): # enables menu actions after file is loaded for action in self.disabled_menu_items: action.setEnabled(True) def slot_limit_results_to_search(self, text): """limit tree view results to search matches reset view after filter to maintain level 1 expansion """ self.search_model_proxy.set_query(text) self.view_tree.reset_ui(should_sort=False) def signal_shim_slot_checkbox_limit_by_changed(self): if self.chk_fcn_scope.isChecked(): self.slot_checkbox_limit_by_changed(Qt.Checked) 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 @param state: checked state """ invoke_reset = True if state == Qt.Checked: minbound, maxbound = util.get_function_boundries_at_current_location( ) if self.range_model_proxy.min_ea == minbound and self.range_model_proxy.max_ea == maxbound: # Seek only changed within current function, avoid resetting tree invoke_reset = False self.limit_results_to_function((minbound, maxbound)) else: self.range_model_proxy.reset_address_range_filter() if invoke_reset: self.view_tree.reset_ui() 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: (tuple (maxbound, minbound)) """ if f: self.range_model_proxy.add_address_range_filter(f[0], f[1]) else: # if function not exists don't display any results (assume address never -1) self.range_model_proxy.add_address_range_filter(-1, -1) # --- Menu Actions def cma_analyze_and_rename(self): message_box = QMessageBox() message_box.setStyleSheet("QLabel{min-width: 370px;}") message_box.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) message_box.setEscapeButton(QMessageBox.Cancel) message_box.setDefaultButton(QMessageBox.Ok) message_box.setWindowTitle('Warning') message_box.setText( 'Depending on the size of the binary and the' ' amount of \n' 'capa matches this feature can take some time to \n' 'complete and might make the UI freeze temporarily.') message_box.setInformativeText('Are you sure you want to proceed ?') ret = message_box.exec_() # Ok = 1024 if ret == 1024: self.model_data.auto_rename_functions() def cma_create_flags(self): self.model_data.create_flags() def cma_display_about(self): c = CAPAExplorerPlugin() info_text = ("{description}\n\n" "https://github.com/ninewayhandshake/capa-explorer\n\n" "Version: {version}\n" "Author: {author}\n" "License: Apache License 2.0\n").format( version=c.version, author=c.author, description=c.description, ) text = CAPAExplorerPlugin().name message_box = QMessageBox() message_box.setStyleSheet("QLabel{min-width: 370px;}") message_box.setWindowTitle('About') message_box.setText(text) message_box.setInformativeText(info_text) message_box.setStandardButtons(QMessageBox.Close) for i in message_box.findChildren(QLabel): i.setFocusPolicy(Qt.NoFocus) message_box.exec_() def cma_load_file(self): filename = QFileDialog.getOpenFileName() path = filename[0] if len(path): try: data = util.load_capa_json(path) self.fill_attack_table(data['rules']) self.model_data.clear() self.model_data.render_capa_doc(data) # Restore ability to scroll on last column self.view_tree.header().setStretchLastSection(False) self.view_tree.slot_resize_columns_to_content() self.enable_menu_items_after_load() except Exception as e: util.log('Could not load json file.') else: util.log('No file selected.')
def _show_menu(self, index, pos): node_id, \ node_name, \ is_online, \ is_itself, \ is_wiped = self._model.get_node_id_online_itself(index) if not node_id: return license_free = self._license_type == FREE_LICENSE menu = QMenu(self._view) menu.setStyleSheet("background-color: #EFEFF4; ") menu.setToolTipsVisible(license_free) if license_free: menu.setStyleSheet( 'QToolTip {{background-color: #222222; color: white;}}') menu.hovered.connect(lambda a: self._on_menu_hovered(a, menu)) def add_menu_item(caption, index=None, action_name=None, action_type="", start_transfers=False, disabled=False, tooltip=""): action = menu.addAction(caption) action.setEnabled(not disabled) action.tooltip = tooltip if tooltip else "" if not start_transfers: action.triggered.connect(lambda: self._on_menu_clicked( index, action_name, action_type)) else: action.triggered.connect(self.start_transfers.emit) tooltip = tr("Not available for free license") \ if license_free and not is_itself else "" if not is_online: action_in_progress = ("hideNode", "") in \ self._nodes_actions.get(node_id, set()) item_text = tr("Remove node") if not action_in_progress \ else tr("Remove node in progress...") add_menu_item(item_text, index, "hideNode", disabled=action_in_progress) elif is_itself: add_menu_item(tr("Transfers..."), start_transfers=True) if not is_wiped: wipe_in_progress = ("execute_remote_action", "wipe") in \ self._nodes_actions.get(node_id, set()) if not wipe_in_progress: action_in_progress = ("execute_remote_action", "logout") in \ self._nodes_actions.get(node_id, set()) item_text = tr("Log out") if not action_in_progress \ else tr("Log out in progress...") add_menu_item(item_text, index, "execute_remote_action", "logout", disabled=action_in_progress or license_free and not is_itself, tooltip=tooltip) item_text = tr("Log out && wipe") if not wipe_in_progress \ else tr("Wipe in progress...") add_menu_item(item_text, index, "execute_remote_action", "wipe", disabled=wipe_in_progress or license_free and not is_itself, tooltip=tooltip) pos_to_show = QPoint(pos.x(), pos.y() + 20) menu.exec_(self._view.mapToGlobal(pos_to_show))