def _init_widgets(self): vertical_layout = QVBoxLayout() # dialog text self.label = QLabel(self) self.label.setWordWrap(True) self.label.setText( "<html><head/><body><p> Magic Sync is a one-time sync that attempts to sync <span style=\" " "font-weight:600;\">non-conflicting</span> data from all users on all functions, essentially a global " "knowledge merge. Would you like to preform this action? You may optionally select a user you would like " "prioritized for non-conflicting sync first.</p><p>Priority User:</p></body></html>", ) # user selection items = self._get_users() self.comboBox = QComboBox(self) self.comboBox.addItems(items) # confirm button self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.No | QDialogButtonBox.Yes) self.buttonBox.accepted.connect(self._on_yes_clicked) self.buttonBox.rejected.connect(self._on_no_clicked) vertical_layout.addWidget(self.label, 0, Qt.AlignBottom) vertical_layout.addWidget(self.comboBox, 0, Qt.AlignBottom) vertical_layout.addWidget(self.buttonBox, 0, Qt.AlignBottom) self._main_layout.addLayout(vertical_layout, 0, 0, 1, 1)
class MagicSyncDialog(QDialog): def __init__(self, controller, parent=None): super().__init__(parent) self.controller = controller self.setWindowTitle("Magic Sync") self._main_layout = QGridLayout() self._init_widgets() self.setLayout(self._main_layout) self.should_sync = False self.preferred_user = None self.show() def _init_widgets(self): vertical_layout = QVBoxLayout() # dialog text self.label = QLabel(self) self.label.setWordWrap(True) self.label.setText( "<html><head/><body><p> Magic Sync is a one-time sync that attempts to sync <span style=\" " "font-weight:600;\">non-conflicting</span> data from all users on all functions, essentially a global " "knowledge merge. Would you like to preform this action? You may optionally select a user you would like " "prioritized for non-conflicting sync first.</p><p>Priority User:</p></body></html>", ) # user selection items = self._get_users() self.comboBox = QComboBox(self) self.comboBox.addItems(items) # confirm button self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.No | QDialogButtonBox.Yes) self.buttonBox.accepted.connect(self._on_yes_clicked) self.buttonBox.rejected.connect(self._on_no_clicked) vertical_layout.addWidget(self.label, 0, Qt.AlignBottom) vertical_layout.addWidget(self.comboBox, 0, Qt.AlignBottom) vertical_layout.addWidget(self.buttonBox, 0, Qt.AlignBottom) self._main_layout.addLayout(vertical_layout, 0, 0, 1, 1) def _get_users(self): return ["None"] + list(self.controller.usernames(priority=1)) def _on_yes_clicked(self): self.should_sync = True combo_text = self.comboBox.currentText() self.preferred_user = combo_text if combo_text != "None" else None self.close() def _on_no_clicked(self): self.should_sync = False self.close()
def _init_widgets(self): # status bar self._status_label = QLabel(self) self._status_label.setText(self.controller.status_string()) self._status_bar = QContextStatusBar(self.controller, self) self._status_bar.addPermanentWidget(self._status_label) # control box control_layout = QVBoxLayout() # tabs for panel_tabs self.tabView = QTabWidget() # add panel_tabs to tabs self._ctx_table = QCTXTable(self.controller) self._func_table = QFunctionTable(self.controller) self._global_table = QGlobalsTable(self.controller) self._activity_table = QActivityTable(self.controller) self._utilities_panel = QUtilPanel(self.controller) self.tabView.addTab(self._ctx_table, "Context") self.tabView.addTab(self._func_table, "Functions") self.tabView.addTab(self._global_table, "Globals") self.tabView.addTab(self._activity_table, "Activity") self.tabView.addTab(self._utilities_panel, "Utilities") self.tables.update({ "functions": self._func_table, "globals": self._global_table, "activity": self._activity_table }) main_layout = QVBoxLayout() main_layout.addWidget(self.tabView) main_layout.addWidget(self._status_bar) main_layout.setSpacing(0) main_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(main_layout)
class ControlPanel(QWidget): update_ready = Signal() ctx_change = Signal() def __init__(self, controller, parent=None): super(ControlPanel, self).__init__(parent) self.controller = controller self.tables = {} self._init_widgets() # register controller callback self.update_ready.connect(self.reload) self.controller.ui_callback = self.update_callback self.ctx_change.connect(self._reload_ctx) self.controller.ctx_change_callback = self.ctx_callback def update_callback(self): """ This function will be called in another thread, so the work done here is guaranteed to be thread safe. @return: """ self._update_table_data() self.update_ready.emit() def ctx_callback(self): if isinstance(self.controller.last_ctx, binsync.data.Function): self._ctx_table.update_table(new_ctx=self.controller.last_ctx.addr) self.ctx_change.emit() def reload(self): # check if connected if self.controller and self.controller.check_client(): self._reload_tables() # update status status = self.controller.status_string( ) if self.controller else "Disconnected" self._status_label.setText(status) def closeEvent(self, event): if self.controller is not None: self.controller.client_init_callback = None def _init_widgets(self): # status bar self._status_label = QLabel(self) self._status_label.setText(self.controller.status_string()) self._status_bar = QContextStatusBar(self.controller, self) self._status_bar.addPermanentWidget(self._status_label) # control box control_layout = QVBoxLayout() # tabs for panel_tabs self.tabView = QTabWidget() # add panel_tabs to tabs self._ctx_table = QCTXTable(self.controller) self._func_table = QFunctionTable(self.controller) self._global_table = QGlobalsTable(self.controller) self._activity_table = QActivityTable(self.controller) self._utilities_panel = QUtilPanel(self.controller) self.tabView.addTab(self._ctx_table, "Context") self.tabView.addTab(self._func_table, "Functions") self.tabView.addTab(self._global_table, "Globals") self.tabView.addTab(self._activity_table, "Activity") self.tabView.addTab(self._utilities_panel, "Utilities") self.tables.update({ "functions": self._func_table, "globals": self._global_table, "activity": self._activity_table }) main_layout = QVBoxLayout() main_layout.addWidget(self.tabView) main_layout.addWidget(self._status_bar) main_layout.setSpacing(0) main_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(main_layout) def _reload_ctx(self): ctx_name = self.controller.last_ctx.name or "" ctx_name = ctx_name[:12] + "..." if len(ctx_name) > 12 else ctx_name self._status_bar.showMessage( f"{ctx_name}@{hex(self.controller.last_ctx.addr)}") self._ctx_table.reload() def _reload_tables(self): for _, table in self.tables.items(): table.reload() self._ctx_table.reload() def _update_table_data(self): for _, table in self.tables.items(): table.update_table() self._ctx_table.update_table()
def _init_widgets(self): upper_layout = QGridLayout() # user label user_label = QLabel(self) user_label.setText("User name") user_label.setToolTip( "The name your user will be saved as on remote. Can be anything other that 'root'. This name does not " "need to be the same as your Git username.") self._user_edit = QLineEdit(self) row = 0 upper_layout.addWidget(user_label, row, 0) upper_layout.addWidget(self._user_edit, row, 1) row += 1 # binsync label binsync_label = QLabel(self) binsync_label.setText("Git repo") binsync_label.setToolTip( "The path to a locally cloned Git repo. This can be ignored if you have not cloned down a remote " "repo yet, but you have the URL. The local repo can also be an empty folder that will be turned into " "a BinSync database by selecting 'init_remote'.") # repo path self._repo_edit = QLineEdit(self) self._repo_edit.textChanged.connect(self._on_repo_textchanged) #self._repo_edit.setFixedWidth(150) # repo path selection button repo_button = QPushButton(self) repo_button.setText("...") repo_button.clicked.connect(self._on_repo_clicked) repo_button.setFixedWidth(40) upper_layout.addWidget(binsync_label, row, 0) upper_layout.addWidget(self._repo_edit, row, 1) upper_layout.addWidget(repo_button, row, 2) row += 1 # clone from a remote URL self.remote_label = QLabel(self) self.remote_label.setText("Remote URL") self.remote_label.setToolTip( "The URL to a remove Git repo. This is not required if you have already selected a locally cloned " "Git repo that has a remote. This repo will be cloned to the same location as your binary if a local " "repo does not exist yet.") self._remote_edit = QLineEdit(self) upper_layout.addWidget(self.remote_label, row, 0) upper_layout.addWidget(self._remote_edit, row, 1) row += 1 # initialize repo checkbox self._initrepo_checkbox = QCheckBox(self) self._initrepo_checkbox.setText("Init Remote") self._initrepo_checkbox.setToolTip( "Ether inits the local folder, making it into a Git repo, or updates the remote references of the current " "Git repo to have the correct BinSync layout which includes the root branch and the new user. Should only " "ever be used when first creating a BinSync repo for all users.") self._initrepo_checkbox.setChecked(False) #self._initrepo_checkbox.setEnabled(False) upper_layout.addWidget(self._initrepo_checkbox, row, 1) row += 1 # buttons self._ok_button = QPushButton(self) self._ok_button.setText("OK") self._ok_button.setDefault(True) self._ok_button.clicked.connect(self._on_ok_clicked) cancel_button = QPushButton(self) cancel_button.setText("Cancel") cancel_button.clicked.connect(self._on_cancel_clicked) buttons_layout = QHBoxLayout() buttons_layout.addWidget(self._ok_button) buttons_layout.addWidget(cancel_button) # main layout self._main_layout.addLayout(upper_layout) self._main_layout.addLayout(buttons_layout) # change the text if config exists if self.should_load_config: self.load_saved_config()
class SyncConfig(QDialog): """ The dialog that allows a user to config a BinSync client for: - initing a local repo - cloning a remote - using a locally pulled remote repo """ def __init__(self, controller, open_magic_sync=True, load_config=True, parent=None): super().__init__(parent) self.controller = controller self.open_magic_sync = open_magic_sync self.should_load_config = load_config self.setWindowTitle("Configure BinSync") self._main_layout = QVBoxLayout() self._user_edit = None # type:QLineEdit self._repo_edit = None # type:QLineEdit self._remote_edit = None # type:QLineEdit self._initrepo_checkbox = None # type:QCheckBox self._init_widgets() self.setLayout(self._main_layout) self.show() def _init_widgets(self): upper_layout = QGridLayout() # user label user_label = QLabel(self) user_label.setText("User name") user_label.setToolTip( "The name your user will be saved as on remote. Can be anything other that 'root'. This name does not " "need to be the same as your Git username.") self._user_edit = QLineEdit(self) row = 0 upper_layout.addWidget(user_label, row, 0) upper_layout.addWidget(self._user_edit, row, 1) row += 1 # binsync label binsync_label = QLabel(self) binsync_label.setText("Git repo") binsync_label.setToolTip( "The path to a locally cloned Git repo. This can be ignored if you have not cloned down a remote " "repo yet, but you have the URL. The local repo can also be an empty folder that will be turned into " "a BinSync database by selecting 'init_remote'.") # repo path self._repo_edit = QLineEdit(self) self._repo_edit.textChanged.connect(self._on_repo_textchanged) #self._repo_edit.setFixedWidth(150) # repo path selection button repo_button = QPushButton(self) repo_button.setText("...") repo_button.clicked.connect(self._on_repo_clicked) repo_button.setFixedWidth(40) upper_layout.addWidget(binsync_label, row, 0) upper_layout.addWidget(self._repo_edit, row, 1) upper_layout.addWidget(repo_button, row, 2) row += 1 # clone from a remote URL self.remote_label = QLabel(self) self.remote_label.setText("Remote URL") self.remote_label.setToolTip( "The URL to a remove Git repo. This is not required if you have already selected a locally cloned " "Git repo that has a remote. This repo will be cloned to the same location as your binary if a local " "repo does not exist yet.") self._remote_edit = QLineEdit(self) upper_layout.addWidget(self.remote_label, row, 0) upper_layout.addWidget(self._remote_edit, row, 1) row += 1 # initialize repo checkbox self._initrepo_checkbox = QCheckBox(self) self._initrepo_checkbox.setText("Init Remote") self._initrepo_checkbox.setToolTip( "Ether inits the local folder, making it into a Git repo, or updates the remote references of the current " "Git repo to have the correct BinSync layout which includes the root branch and the new user. Should only " "ever be used when first creating a BinSync repo for all users.") self._initrepo_checkbox.setChecked(False) #self._initrepo_checkbox.setEnabled(False) upper_layout.addWidget(self._initrepo_checkbox, row, 1) row += 1 # buttons self._ok_button = QPushButton(self) self._ok_button.setText("OK") self._ok_button.setDefault(True) self._ok_button.clicked.connect(self._on_ok_clicked) cancel_button = QPushButton(self) cancel_button.setText("Cancel") cancel_button.clicked.connect(self._on_cancel_clicked) buttons_layout = QHBoxLayout() buttons_layout.addWidget(self._ok_button) buttons_layout.addWidget(cancel_button) # main layout self._main_layout.addLayout(upper_layout) self._main_layout.addLayout(buttons_layout) # change the text if config exists if self.should_load_config: self.load_saved_config() # # Event handlers # def _on_ok_clicked(self): user = self._user_edit.text() path = self._repo_edit.text() remote_url = self._remote_edit.text() init_repo = self._initrepo_checkbox.isChecked() l.debug( "Attempting to connect to/init repo, user: %s | path: %s | init_repo? %r", user, path, init_repo) if not user: QMessageBox(self).critical(None, "Invalid user name", "User name cannot be empty.") return if user.lower() == "__root__": QMessageBox(self).critical( None, "Invalid user name", "User name cannot (and should not) be \'__root__\'.") return if not remote_url and not os.path.isdir(path) and not init_repo: QMessageBox(self).critical( None, "Repo does not exist", "The specified sync directory does not exist. " "Do you maybe want to initialize it?") return # convert to remote repo if no local is provided if self.is_git_repo(path): remote_url = None if remote_url and not path: path = os.path.join( os.path.dirname(self.controller.binary_path() or ""), os.path.basename(self.controller.binary_path() or "") + "_bs") try: connection_warnings = self.controller.connect( user, path, init_repo=init_repo, remote_url=remote_url) pass except Exception as e: l.critical("Error connecting to specified repository!") QMessageBox(self).critical(None, "Error connecting to repository", str(e)) traceback.print_exc() return # # controller is now successfully connected to a real BinSync client. Everything from this point # onwards assumes that all normal client properties and functions work. # # warn user of anything that might look off self._parse_and_display_connection_warnings(connection_warnings) l.info(f"Client has connected to sync repo with user: {user}.") # create and save config if possible saved_config = self.save_config() if saved_config: l.debug(f"Configuration file was saved to {saved_config}.") self.close() def _on_repo_clicked(self): if 'SNAP' in os.environ: directory = QFileDialog.getExistingDirectory( self, "Select sync repo", "", QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks | QFileDialog.DontUseNativeDialog) else: directory = QFileDialog.getExistingDirectory( self, "Select sync repo", "", QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) self._repo_edit.setText(QDir.toNativeSeparators(directory)) def _on_repo_textchanged(self, new_text): path = new_text if pathlib.Path(path).exists() and self.is_git_repo(path): repo = git.Repo(path) try: url = repo.remote().url except Exception: url = None self._remote_edit.setText(url or "") self._remote_edit.setEnabled(False) self.remote_label.setStyleSheet('color: gray') is_binsync_inited = False try: is_binsync_inited = any( ref.name.endswith(BINSYNC_ROOT_BRANCH) for ref in repo.remote().refs) except Exception: pass if is_binsync_inited: self._initrepo_checkbox.setChecked(False) self._initrepo_checkbox.setStyleSheet( 'QCheckBox::unchecked { color: gray } ' 'QCheckBox::checked { color: gray }') self._initrepo_checkbox.setEnabled(False) else: self._initrepo_checkbox.setEnabled(True) self._initrepo_checkbox.setStyleSheet( 'QCheckBox::unchecked { color: white } ' 'QCheckBox::checked { color: white }') else: self._remote_edit.setEnabled(True) self._initrepo_checkbox.setEnabled(True) self.remote_label.setStyleSheet('color: white') self._initrepo_checkbox.setStyleSheet( 'QCheckBox::unchecked { color: white } ' 'QCheckBox::checked { color: white }') def _on_cancel_clicked(self): self.close() # # Utils # def load_saved_config(self) -> bool: config = ProjectConfig.load_from_file(self.controller.binary_path() or "") if not config: return False user = config.user or "" repo = config.repo_path or "" remote = config.remote if config.remote and not config.repo_path else "" self._user_edit.setText(user) self._repo_edit.setText(repo) self._on_repo_textchanged(repo) self._remote_edit.setText(remote) return True def save_config(self) -> Optional[str]: user = self._user_edit.text() remote = self._remote_edit.text() repo = self._repo_edit.text() if remote and not repo: repo = str( pathlib.Path(self.controller.client.repo_root).absolute()) config = ProjectConfig(self.controller.binary_path() or "", user=user, repo_path=repo, remote=remote) if not config: return config return config.save() # # Static methods # @staticmethod def is_git_repo(path): return os.path.isdir(os.path.join(path, ".git")) @staticmethod def _parse_and_display_connection_warnings(warnings): warning_text = "" for warning in warnings: if warning == ConnectionWarnings.HASH_MISMATCH: warning_text += "Warning: the hash stored for this BinSync project does not match " \ "the hash of the binary you are attempting to analyze. It's possible " \ "you are working on a different binary.\n" if len(warning_text) > 0: QMessageBox.warning( None, "BinSync: Connection Warnings", warning_text, QMessageBox.Ok, )
def _init_widgets(self): # # Developer Options Group # dev_options_group = QGroupBox() dev_options_layout = QVBoxLayout() dev_options_group.setTitle("Developer Options") dev_options_group.setLayout(dev_options_layout) self._debug_log_toggle = QCheckBox("Toggle Debug Logging") self._debug_log_toggle.setToolTip( "Toggles the logging of events BinSync developers care about.") self._debug_log_toggle.stateChanged.connect(self._handle_debug_toggle) dev_options_layout.addWidget(self._debug_log_toggle) # # Sync Options Group # sync_options_group = QGroupBox() sync_options_layout = QVBoxLayout() sync_options_group.setTitle("Sync Options") sync_options_group.setLayout(sync_options_layout) self._merge_level_label = QLabel("Sync Merge Level") self._merge_level_label.setToolTip("""<html> <p> Defines which method is used to sync artifacts from another user.<br> <b>Non-Conflicting</b>: Only syncs artifacts that are not currently defined by you, so nothing is ever overwritten.<br> <b>Overwrite</b>: Syncs all artifacts regardless of your defined ones, overwriting everything.<br> <b>Merge</b>: You pick which artifacts are synced via the UI. <b>Unimplemented.</b> </p> </html> """) self._merge_level_label.setTextFormat(Qt.RichText) self._merge_level_combobox = QComboBox() self._merge_level_combobox.addItems( ["Non-Conflicting", "Overwrite", "Merge"]) self._merge_level_combobox.currentIndexChanged.connect( self._handle_sync_level_change) sync_level_layout = QHBoxLayout() #sync_level_group.layout().setContentsMargins(0, 0, 0, 0) sync_level_layout.addWidget(self._merge_level_label) sync_level_layout.addWidget(self._merge_level_combobox) self._magic_sync_button = QPushButton("Initiate Magic Sync") self._magic_sync_button.clicked.connect(self._handle_magic_sync_button) self._magic_sync_button.setToolTip( "Performs a best effort merge of all existing user data to your state, " "but won't affect your existing state (this uses a non-conflicting merge)." ) self._force_push_button = QPushButton("Force Push...") self._force_push_button.clicked.connect(self._handle_force_push_button) self._force_push_button.setToolTip( "Manually select function and globals you would like to be force committed " "and pushed to your user branch on Git.") sync_options_layout.addLayout(sync_level_layout) sync_options_group.layout().addWidget(self._magic_sync_button) sync_options_group.layout().addWidget(self._force_push_button) # # Final Layout # main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 20, 10, 20) main_layout.setSpacing(18) main_layout.setAlignment(Qt.AlignTop) main_layout.addWidget(sync_options_group) main_layout.addWidget(dev_options_group) self.setLayout(main_layout)
class QUtilPanel(QWidget): def __init__(self, controller: BinSyncController, parent=None): super().__init__(parent) self.controller = controller self._init_widgets() def _init_widgets(self): # # Developer Options Group # dev_options_group = QGroupBox() dev_options_layout = QVBoxLayout() dev_options_group.setTitle("Developer Options") dev_options_group.setLayout(dev_options_layout) self._debug_log_toggle = QCheckBox("Toggle Debug Logging") self._debug_log_toggle.setToolTip( "Toggles the logging of events BinSync developers care about.") self._debug_log_toggle.stateChanged.connect(self._handle_debug_toggle) dev_options_layout.addWidget(self._debug_log_toggle) # # Sync Options Group # sync_options_group = QGroupBox() sync_options_layout = QVBoxLayout() sync_options_group.setTitle("Sync Options") sync_options_group.setLayout(sync_options_layout) self._merge_level_label = QLabel("Sync Merge Level") self._merge_level_label.setToolTip("""<html> <p> Defines which method is used to sync artifacts from another user.<br> <b>Non-Conflicting</b>: Only syncs artifacts that are not currently defined by you, so nothing is ever overwritten.<br> <b>Overwrite</b>: Syncs all artifacts regardless of your defined ones, overwriting everything.<br> <b>Merge</b>: You pick which artifacts are synced via the UI. <b>Unimplemented.</b> </p> </html> """) self._merge_level_label.setTextFormat(Qt.RichText) self._merge_level_combobox = QComboBox() self._merge_level_combobox.addItems( ["Non-Conflicting", "Overwrite", "Merge"]) self._merge_level_combobox.currentIndexChanged.connect( self._handle_sync_level_change) sync_level_layout = QHBoxLayout() #sync_level_group.layout().setContentsMargins(0, 0, 0, 0) sync_level_layout.addWidget(self._merge_level_label) sync_level_layout.addWidget(self._merge_level_combobox) self._magic_sync_button = QPushButton("Initiate Magic Sync") self._magic_sync_button.clicked.connect(self._handle_magic_sync_button) self._magic_sync_button.setToolTip( "Performs a best effort merge of all existing user data to your state, " "but won't affect your existing state (this uses a non-conflicting merge)." ) self._force_push_button = QPushButton("Force Push...") self._force_push_button.clicked.connect(self._handle_force_push_button) self._force_push_button.setToolTip( "Manually select function and globals you would like to be force committed " "and pushed to your user branch on Git.") sync_options_layout.addLayout(sync_level_layout) sync_options_group.layout().addWidget(self._magic_sync_button) sync_options_group.layout().addWidget(self._force_push_button) # # Final Layout # main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 20, 10, 20) main_layout.setSpacing(18) main_layout.setAlignment(Qt.AlignTop) main_layout.addWidget(sync_options_group) main_layout.addWidget(dev_options_group) self.setLayout(main_layout) # # Event Handlers # def _handle_debug_toggle(self): if self._debug_log_toggle.isChecked(): logging.getLogger("binsync").setLevel("DEBUG") logging.getLogger("ida_binsync").setLevel("DEBUG") l.info("Logger has been set to level: DEBUG") else: logging.getLogger("binsync").setLevel("INFO") logging.getLogger("ida_binsync").setLevel("INFO") l.info("Logger has been set to level: INFO") def _handle_sync_level_change(self, index): selected_opt = self._merge_level_combobox.itemText(index) if selected_opt == "Non-Conflicting": self.controller.merge_level = MergeLevel.NON_CONFLICTING elif selected_opt == "Overwrite": self.controller.merge_level = MergeLevel.OVERWRITE elif selected_opt == "Merge": self.controller.merge_level = MergeLevel.MERGE else: return l.debug(f"Sync level changed to: {selected_opt}") def _handle_magic_sync_button(self): dialog = MagicSyncDialog(self.controller) dialog.exec_() if not dialog.should_sync: return self.controller.magic_fill(preference_user=dialog.preferred_user) def _handle_force_push_button(self): self.popup = ForcePushUI(self.controller) self.popup.show()