# Copyright 2015-2016, Tresys Technology, LLC # SPDX-License-Identifier: LGPL-2.1-only from collections import defaultdict from contextlib import suppress from functools import partial import json import logging import os import sys import typing import pkg_resources from PyQt6 import QtCore, QtGui, QtWidgets from setools import __version__, PermissionMap, SELinuxPolicy from . import config, widgets # Supported analyses. These are not directly used here, but # will init the tab registry in widgets.tab for apol's analyses. # pylint: disable=unused-import from .widgets import (boolquery, constraintquery, fsusequery, genfsconquery, ibendportconquery, ibpkeyconquery, infoflow, initsidquery, mlsrulequery, netifconquery, nodeconquery, portconquery, rbacrulequery, summary, terulequery) STYLESHEET: typing.Final = "apol.css" # Class of the tab that opens automatically when a policy is loaded. INITIAL_TAB: typing.Final = summary.SummaryTab # keys for workspace save file SETTINGS_POLICY: typing.Final = "__policy__" SETTINGS_PERMMAP: typing.Final = "__permmap__" SETTINGS_TABS_LIST: typing.Final = "__tabs__" SETTINGS_TAB_TITLE: typing.Final = "__title__" SETTINGS_TAB_CLASS: typing.Final = "__tab__" class ApolWorkspace(QtWidgets.QTabWidget): policy: SELinuxPolicy | None permmap: PermissionMap | None policy_changed = QtCore.pyqtSignal(SELinuxPolicy) permmap_changed = QtCore.pyqtSignal(PermissionMap) def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: # __init__ here to type narrow the parent to the Apol main window super().__init__(parent) self.log = logging.getLogger(__name__) self.permmap = None self.policy = None self.config: typing.Final = config.ApolConfig() self.setAutoFillBackground(True) self.setTabPosition(QtWidgets.QTabWidget.TabPosition.North) self.setTabsClosable(True) self.setMovable(True) self.setCurrentIndex(-1) # counter separate to the open tab count. This increments for each new # tab so we can create a unique title for each tab, even if there are # many tabs with the same analysis. self.tab_counter = 0 # set up tab name editor self.tab_editor = QtWidgets.QLineEdit(self) self.tab_editor.setWindowFlags(QtCore.Qt.WindowType.Popup) self.tab_editor.editingFinished.connect(self.rename_tab) # # Set up workspace actions. These will be pulled in by the # main window and added to the workspace menu. # self.open_policy_action = QtGui.QAction(self) self.open_policy_action.setIcon( QtGui.QIcon.fromTheme("dialog-open", self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_DialogOpenButton))) self.open_policy_action.setIconVisibleInMenu(True) self.open_policy_action.setText("&Open Policy") self.open_policy_action.setToolTip("Open an SELinux Policy") self.open_policy_action.setShortcut("Ctrl+O") self.open_policy_action.triggered.connect(self.select_policy) self.exit_apol_action = QtGui.QAction(self) self.exit_apol_action.setText("E&xit") self.exit_apol_action.setShortcut("Ctrl+Q") self.exit_apol_action.setIcon( QtGui.QIcon.fromTheme("dialog-close", self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_DialogCloseButton))) self.exit_apol_action.triggered.connect(self.parent().close) # type: ignore self.new_analysis_action = QtGui.QAction(self) self.new_analysis_action.setIcon( QtGui.QIcon.fromTheme("file-icon", self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_FileIcon))) self.new_analysis_action.setIconVisibleInMenu(True) self.new_analysis_action.setText("New Analysis") self.new_analysis_action.setToolTip("Start a new analysis on this policy.") self.new_analysis_action.setShortcut("Ctrl+N") self.new_analysis_action.triggered.connect(self.choose_analysis) self.new_from_settings_action = QtGui.QAction(self) self.new_from_settings_action.setText("New Analysis From Settings") self.new_from_settings_action.setToolTip("Start a new analysis using settings from a file.") self.new_from_settings_action.setShortcut("Ctrl+Shift+N") self.new_from_settings_action.triggered.connect(self.new_analysis_from_config) self.save_settings_action = QtGui.QAction(self) self.save_settings_action.setText("Save Tab Settings") self.save_settings_action.setToolTip("Save the current tab\'s settings to file.") self.save_settings_action.setShortcut("Ctrl+S") self.save_settings_action.triggered.connect(self.save_settings) self.load_settings_action = QtGui.QAction(self) self.load_settings_action.setText("Load Tab Settings") self.load_settings_action.setToolTip("Load settings for the current tab.") self.load_settings_action.setShortcut("Ctrl+L") self.load_settings_action.triggered.connect(self.load_settings) self.dupe_tab_action = QtGui.QAction(self) self.dupe_tab_action.setText("&Duplicate Tab") self.dupe_tab_action.setToolTip("Duplicate the active tab.") self.dupe_tab_action.setShortcut("Ctrl+Shift+K") self.dupe_tab_action.triggered.connect(self.dupe_tab) self.close_tab_action = QtGui.QAction(self) self.close_tab_action.setText("&Close Tab") self.close_tab_action.setToolTip("Close the active tab.") self.close_tab_action.setShortcut("Ctrl+W") self.close_tab_action.triggered.connect(self.close_tab) self.load_workspace_action = QtGui.QAction(self) self.load_workspace_action.setText("Load Workspace") self.load_workspace_action.setToolTip("Load workspace from file.") self.load_workspace_action.setShortcut("Ctrl+Shift+L") self.load_workspace_action.triggered.connect(self.load_workspace) self.save_workspace_action = QtGui.QAction(self) self.save_workspace_action.setText("Save Workspace") self.save_workspace_action.setToolTip("Save workspace to file.") self.save_workspace_action.setShortcut("Ctrl+Shift+S") self.save_workspace_action.triggered.connect(self.save_workspace) self.help_action = QtWidgets.QWhatsThis.createAction(self) self.about_apol_action = QtGui.QAction(self) self.about_apol_action.setText("About Apol") self.about_apol_action.triggered.connect(self.about_apol) self.cut_action = QtGui.QAction(self) self.cut_action.setText("Cut") self.cut_action.setShortcut("Ctrl+X") self.cut_action.triggered.connect(self.cut) self.copy_action = QtGui.QAction(self) self.copy_action.setText("Copy") self.copy_action.setShortcut("Ctrl+C") self.copy_action.triggered.connect(self.copy) self.paste_action = QtGui.QAction(self) self.paste_action.setText("Paste") self.paste_action.setShortcut("Ctrl+V") self.paste_action.triggered.connect(self.paste) self.open_permmap = QtGui.QAction(self) self.open_permmap.setText("Open Permission Map") self.open_permmap.setToolTip("Open permission map used for information flow analysis") self.open_permmap.triggered.connect(self.select_permmap) # these two tab actions are to have a global shortcut and # entries in the workspace menu. self.close_policy_action = QtGui.QAction(self) self.close_policy_action.setText("Close Policy") self.close_policy_action.setToolTip("Close the current policy. Closes all analyses too.") self.close_policy_action.triggered.connect(self.close_policy) self.edit_permmap_action = QtGui.QAction(self) self.edit_permmap_action.setText("Edit Permission Map") self.edit_permmap_action.triggered.connect(self.edit_permmap) self.save_permmap_action = QtGui.QAction(self) self.save_permmap_action.setText("Save Permission Map") self.save_permmap_action.triggered.connect(self.save_permmap) # File menu self.menu_File = QtWidgets.QMenu(self) self.menu_File.setTitle("&File") self.menu_File.addAction(self.open_policy_action) self.menu_File.addAction(self.close_policy_action) self.menu_File.addSeparator() self.menu_File.addAction(self.exit_apol_action) # Workspace menu self.menuWorkspace = QtWidgets.QMenu(self) self.menuWorkspace.setTitle("Workspace") self.menuWorkspace.addAction(self.new_analysis_action) self.menuWorkspace.addAction(self.new_from_settings_action) self.menuWorkspace.addSeparator() self.menuWorkspace.addAction(self.load_settings_action) self.menuWorkspace.addAction(self.save_settings_action) self.menuWorkspace.addAction(self.dupe_tab_action) self.menuWorkspace.addAction(self.close_tab_action) self.menuWorkspace.addSeparator() self.menuWorkspace.addAction(self.load_workspace_action) self.menuWorkspace.addAction(self.save_workspace_action) # Edit menu self.menu_Edit = QtWidgets.QMenu(self) self.menu_Edit.setTitle("&Edit") self.menu_Edit.addAction(self.cut_action) self.menu_Edit.addAction(self.copy_action) self.menu_Edit.addAction(self.paste_action) # Permission Map menu self.menuPerm_Map = QtWidgets.QMenu(self) self.menuPerm_Map.setTitle("Permission &Map") self.menuPerm_Map.addAction(self.open_permmap) self.menuPerm_Map.addAction(self.edit_permmap_action) self.menuPerm_Map.addAction(self.save_permmap_action) # Help menu self.menu_Help = QtWidgets.QMenu(self) self.menu_Help.setTitle("&Help") self.menu_Help.addAction(self.help_action) self.menu_Help.addSeparator() self.menu_Help.addAction(self.about_apol_action) self.addAction(self.menu_File.menuAction()) self.addAction(self.menuWorkspace.menuAction()) self.addAction(self.menu_Edit.menuAction()) self.addAction(self.menuPerm_Map.menuAction()) self.addAction(self.menu_Help.menuAction()) # # Add tab context menu # tab_bar = self.tabBar() tab_bar.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) tab_bar.customContextMenuRequested.connect(self.tab_bar_context_menu) # # Connect signals # self.policy_changed.connect(self.update_window_title) self.policy_changed.connect(self.handle_policy_change) self.tabCloseRequested.connect(self.close_tab) self.tabBarDoubleClicked.connect(self.tab_name_editor) QtCore.QMetaObject.connectSlotsByName(self) # # Reimplemented methods for typing purposes # # @typing.override def style(self) -> QtWidgets.QStyle: """Type-narrowed style() method.""" style = super().style() assert style, "No style set, this is an SETools bug" return style # @typing.override def tabBar(self) -> QtWidgets.QTabBar: """Type-narrowed tabBar() method.""" tab_bar = super().tabBar() # seems to be a PyQt 5 bug that "assert tab_bar" fails on QTabBar objects assert tab_bar is not None, "No tab bar set, this is an SETools bug" return tab_bar # @typing.override def widget(self, index: int) -> widgets.tab.BaseAnalysisTabWidget: """Return the widget at the specified tab index.""" return typing.cast(widgets.tab.BaseAnalysisTabWidget, super().widget(index)) # # Main window handling # def update_window_title(self) -> None: """Update window title based on opened policy path.""" with suppress(AssertionError): parent = self.parentWidget() assert isinstance(parent, QtWidgets.QMainWindow) # type narrowing if self.policy: parent.setWindowTitle(f"{self.policy} - apol") else: parent.setWindowTitle("apol") # # Policy handling # def select_policy(self): """Open a file chooser to select a policy file.""" if self.policy and self.count() > 0: reply = QtWidgets.QMessageBox.question( self, "Continue?", "Loading a policy will close all existing analyses. Continue?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return filename = QtWidgets.QFileDialog.getOpenFileName( self, "Open policy file", ".", "SELinux Policies (policy.* sepolicy);;" "All Files (*)")[0] if filename: self.load_policy(filename) def load_policy(self, filename) -> None: """Load a policy file.""" try: self.policy = SELinuxPolicy(filename) self.policy_changed.emit(self.policy) if self.permmap: with suppress(Exception): self.permmap.map_policy(self.policy) self.permmap_changed.emit(self.permmap) except Exception as ex: self.log.critical(f"Failed to load policy \"{filename}\"") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical(self, "Policy loading error", str(ex)) def close_policy(self): """Close the current policy.""" if self.count() > 0: reply = QtWidgets.QMessageBox.question( self, "Continue?", "Closing a policy will close all existing analyses. Continue?", QtWidgets.QMessageBox.StandardButtons() | QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return self.policy = None self.clear() # # Permission map handling # def select_permmap(self): """Open a file chooser to select a permission map file.""" filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open permission map file", ".")[0] if filename: self.load_permmap(filename) def load_permmap(self, filename=None): """Load a permission map file.""" try: self.permmap = PermissionMap(filename) if self.policy: with suppress(Exception): self.permmap.map_policy(self.policy) self.permmap_changed.emit(self.permmap) except Exception as ex: self.log.critical(f"Failed to load default permission map: {ex}") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical( self, "Permission map loading error", str(ex)) def edit_permmap(self): """Open the permission map editor.""" if not self.permmap: QtWidgets.QMessageBox().critical( self, "No open permission map", "Cannot edit permission map. Please open a map first.") self.select_permmap() # in case user cancels out of choosing a permmap, recheck if self.permmap: editor = widgets.permmap.PermissionMapEditor(self.permmap, edit=True, parent=self) editor.apply_permmap.connect(self.permmap_changed) editor.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) editor.show() def save_permmap(self): """Save the permission map to a file.""" path = str(self.permmap) if self.permmap else "perm_map" filename = QtWidgets.QFileDialog.getSaveFileName(self, "Save permission map file", path)[0] if filename: try: self.permmap.save(filename) except Exception as ex: self.log.critical(f"Failed to save permission map: {ex}") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical(self, "Permission map saving error", str(ex)) # # Tab handling # def choose_analysis(self): """Open a dialog to choose an analysis.""" if not self.policy: QtWidgets.QMessageBox().critical( self, "No open policy", "Cannot start a new analysis. Please open a policy first.") self.select_policy() if self.policy: # this check of self._policy is here in case someone # tries to start an analysis with no policy open, but then # cancels out of the policy file chooser or there is an # error opening the policy file. ChooseAnalysis(self.policy.mls, parent=self) def create_new_analysis(self, tab_class: type[widgets.tab.BaseAnalysisTabWidget]) -> int: """Create a new analysis tab with the selected widget.""" self.tab_counter += 1 counted_name = f"{self.tab_counter}: {tab_class.tab_title}" assert self.policy assert self.permmap new_tab = tab_class(self.policy, self.permmap, parent=self) new_tab.setObjectName(counted_name) self.permmap_changed.connect(new_tab.handle_permmap_change) index = self.addTab(new_tab, counted_name) self.setTabToolTip(index, tab_class.tab_title) self.setCurrentIndex(index) self.toggle_workspace_actions() return index def tab_bar_context_menu(self, pos: QtCore.QPoint) -> None: """Display a context menu for the tab bar.""" tab_bar = self.tabBar() index = tab_bar.tabAt(pos) # # Generate context menu for this specific tab index, which may not # be the active tab. # rename_tab_action = QtGui.QAction(self) rename_tab_action.setText("&Rename Tab") rename_tab_action.setToolTip("Rename this tab.") rename_tab_action.triggered.connect(partial(self.tab_name_editor, index)) dupe_tab_action = QtGui.QAction(self) dupe_tab_action.setText("&Duplicate Tab") dupe_tab_action.setToolTip("Duplicate this tab.") dupe_tab_action.triggered.connect(partial(self.dupe_tab, index)) close_tab_action = QtGui.QAction(self) close_tab_action.setText("&Close Tab") close_tab_action.setToolTip("Close this tab.") close_tab_action.triggered.connect(partial(self.close_tab, index)) menu = QtWidgets.QMenu(self) menu.addAction(rename_tab_action) menu.addAction(dupe_tab_action) menu.addAction(close_tab_action) menu.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) menu.popup(tab_bar.mapToGlobal(pos)) def tab_name_editor(self, index: int | None = None) -> None: """Open the tab name editor for the specified tab index.""" if index is None: index = self.currentIndex() if index < 0: return tab_area = self.tabBar().tabRect(index) self.tab_editor.move(self.mapToGlobal(tab_area.topLeft())) self.tab_editor.setText(self.tabText(index)) self.tab_editor.selectAll() self.tab_editor.show() self.tab_editor.setFocus() def dupe_tab(self, index: int | None = None) -> None: """ Duplicate a tab specified by index. If no index is specified, the active one is used. """ if index is None: index = self.currentIndex() if index < 0: return settings = self._get_settings(index) new_index = self.create_new_analysis(type(self.widget(index))) self._put_settings(settings, new_index) def close_tab(self, index: int | None = None) -> None: """Close a tab specified by index.""" if index is None: index = self.currentIndex() if index < 0: return widget = self.widget(index) widget.close() self.removeTab(index) self.toggle_workspace_actions() def rename_tab(self) -> None: """Rename the active tab.""" # this should never be negative since the editor is modal index = self.currentIndex() assert index >= 0, "Tab index is negative in rename_tab. This is an SETools bug." tab = self.widget(index) title = self.tab_editor.text() self.tab_editor.hide() self.setTabText(index, title) tab.setObjectName(title) # # Workspace actions # # @typing.override def clear(self) -> None: """Close all tabs.""" super().clear() self.toggle_workspace_actions() def handle_policy_change(self, policy: SELinuxPolicy) -> None: """Handle a policy change. Close all tabs and create new initial tab.""" self.log.debug(f"Received policy change signal to {policy}.") self.clear() # Open up a new instance of the initial tab if no tabs exist. self.log.debug(f"Opening new {INITIAL_TAB} tab.") self.create_new_analysis(INITIAL_TAB) def toggle_workspace_actions(self) -> None: """ Enable or disable workspace actions depending on how many tabs are open and if a policy is open. This is a slot for the QTabWidget.currentChanged() signal, though index is ignored. """ open_tabs = self.count() > 0 open_policy = self.policy is not None self.log.debug(f"{'Enabling' if open_policy else 'Disabling'} actions requiring " "an open policy.") self.log.debug(f"{'Enabling' if open_tabs else 'Disabling'} actions requiring open tabs.") self.save_settings_action.setEnabled(open_tabs) self.save_workspace_action.setEnabled(open_tabs) self.new_analysis_action.setEnabled(open_policy) self.new_from_settings_action.setEnabled(open_policy) self.load_settings_action.setEnabled(open_tabs) self.close_policy_action.setEnabled(open_policy) def _get_settings(self, index: int | None = None) -> dict: """Return a dictionary with the settings of the tab at the specified index.""" if index is None: index = self.currentIndex() assert index >= 0, "Tab index is negative in _get_settings. This is an SETools bug." tab = self.widget(index) settings = tab.save() # add the tab info to the settings. settings[SETTINGS_TAB_TITLE] = self.tabText(index) settings[SETTINGS_TAB_CLASS] = type(tab).__name__ return settings def _put_settings(self, settings: dict, index: int | None = None): """Load the settings into the specified tab.""" if index is None: index = self.currentIndex() assert index >= 0, "Tab index is negative in _put_settings. This is an SETools bug." tab = self.widget(index) if settings[SETTINGS_TAB_CLASS] != type(tab).__name__: raise TypeError(f"The current tab ({type(tab).__name__}) does not match the tab in " f"the settings file ({settings[SETTINGS_TAB_CLASS]}).") try: self.setTabText(index, str(settings[SETTINGS_TAB_TITLE])) except KeyError: self.log.warning("Settings file does not have a title setting.") tab.load(settings) def load_settings(self, new: bool = False): """Open a file chooser and load settings from the chosen file.""" filename = QtWidgets.QFileDialog.getOpenFileName( self, "Open settings file", ".", "Apol Tab Settings File (*.apolt);;" "All Files (*)")[0] if not filename: return try: with open(filename, "r") as fd: settings = json.load(fd) except ValueError: self.log.critical(f"Invalid settings file \"{filename}\"") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical( self, "Failed to load settings", f"Invalid settings file: \"{filename}\"") return except OSError as ex: self.log.critical(f"Unable to load settings file \"{ex.filename}\": {ex.strerror}") QtWidgets.QMessageBox().critical( self, "Failed to load settings", f"Failed to load \"{ex.filename}\": {ex.strerror}") return except Exception as ex: self.log.critical("Unable to load settings file \"{filename}\": {ex}") QtWidgets.QMessageBox().critical( self, "Failed to load settings", str(ex)) return self.log.info(f"Loading analysis settings from \"{filename}\"") if new: try: tabclass = widgets.tab.TAB_REGISTRY[settings[SETTINGS_TAB_CLASS]] except KeyError: self.log.critical(f"Missing analysis type in \"{filename}\"") QtWidgets.QMessageBox().critical( self, "Failed to load settings", "The type of analysis is missing in the settings file.") return # The tab title will be set by _put_settings. index = self.create_new_analysis(tabclass) else: index = None try: self._put_settings(settings, index) except Exception as ex: self.log.critical(f"Error loading settings file \"{filename}\": {ex}") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical( self, "Failed to load settings", f"Error loading settings file \"{filename}\":\n\n{ex}") else: self.log.info(f"Successfully loaded analysis settings from \"{filename}\"") def new_analysis_from_config(self): """Create a new analysis tab from the settings in the config file.""" self.load_settings(new=True) def save_settings(self): """Open a file chooser and save settings to the chosen file.""" try: settings = self._get_settings() except widgets.exception.TabFieldError as ex: self.log.critical(f"Errors in the query prevent saving the settings. {ex}") QtWidgets.QMessageBox().critical( self, "Unable to save settings", "Please resolve errors in the tab before saving the settings.") return filename = QtWidgets.QFileDialog.getSaveFileName( self, "Save analysis tab settings", "analysis.apolt", "Apol Tab Settings File (*.apolt);;" "All Files (*)")[0] if not filename: return try: with open(filename, "w") as fd: json.dump(settings, fd, indent=1) except OSError as ex: self.log.critical(f"Unable to save settings file \"{ex.filename}\": {ex.strerror}") QtWidgets.QMessageBox().critical( self, "Failed to save settings", f"Failed to save \"{ex.filename}\": {ex.strerror}") except Exception as ex: self.log.critical(f"Unable to save settings file \"{filename}\": {ex}") QtWidgets.QMessageBox().critical( self, "Failed to save settings", str(ex)) else: self.log.info(f"Successfully saved settings file \"{filename}\"") def load_workspace(self): """Open a file chooser and load a workspace from the chosen file.""" # 1. if number of tabs > 0, check if we really want to do this if self.count() > 0: reply = QtWidgets.QMessageBox.question( self, "Continue?", "Loading a workspace will close all existing analyses. Continue?", QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.StandardButton.Yes) | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return # 2. try to load the workspace file, if we fail, bail filename = QtWidgets.QFileDialog.getOpenFileName( self, "Open workspace file", ".", "Apol Workspace Files (*.apolw);;" "All Files (*)")[0] if not filename: return try: with open(filename, "r") as fd: workspace = json.load(fd) except ValueError: self.log.critical(f"Invalid workspace file \"{filename}\"") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical( self, "Failed to load workspace", f"Invalid workspace file: \"{filename}\"") return except OSError as ex: self.log.critical(f"Unable to load workspace file \"{ex.filename}\": {ex.strerror}") QtWidgets.QMessageBox().critical( self, "Failed to load workspace", f"Failed to load \"{ex.filename}\": {ex.strerror}") return except Exception as ex: self.log.critical(f"Unable to load workspace file \"{filename}\": {ex}") self.log.debug("Backtrace", exc_info=True) QtWidgets.QMessageBox().critical(self, "Failed to load workspace", str(ex)) return # 3. close all tabs. Explicitly do this to avoid the question # about closing the policy with tabs open. self.clear() # 4. close policy self.close_policy() # 5. try to open the specified policy, if we fail, bail. Note: # handling exceptions from the policy load is done inside # the load_policy function, so only the KeyError needs to be caught here try: self.load_policy(workspace[SETTINGS_POLICY]) except KeyError: self.log.critical(f"Missing policy in workspace file \"{filename}\"") QtWidgets.QMessageBox().critical( self, "Aborting workspace load.", f"Missing policy in workspace file \"{filename}\"") if self.policy is None: self.log.critical(f"The policy could not be loaded in workspace file \"{filename}\"") QtWidgets.QMessageBox().critical( self, "Aborting workspace load.", f"The policy could not be loaded in workspace file \"{filename}\".") return # 6. try to open the specified perm map, if we fail, # tell the user we will continue with the default map; load the default map # Note: handling exceptions from the map load is done inside # the load_permmap function, so only the KeyError needs to be caught here try: self.load_permmap(workspace[SETTINGS_PERMMAP]) except KeyError: self.log.warning(f"Missing permission map in workspace file \"{filename}\"") QtWidgets.QMessageBox().warning( self, "Missing permission map setting.", f"Missing permission map in workspace file \"{filename}\". " "Loading default permission map.") if self.permmap is None: self.load_permmap() # 7. try to open all tabs and apply settings. Record any errors try: tab_list = list(workspace[SETTINGS_TABS_LIST]) except KeyError: self.log.critical(f"Missing tab list in workspace file \"{filename}\"") QtWidgets.QMessageBox().critical( self, "Failed to load workspace", "The workspace file is missing the tab list. Aborting.") return except TypeError: self.log.critical("Invalid tab list in workspace file.") QtWidgets.QMessageBox().critical( self, "Failed to load workspace", "The tab count is invalid. Aborting.") return loading_errors = [] for i, settings in enumerate(tab_list): try: tabclass = widgets.tab.TAB_REGISTRY[settings[SETTINGS_TAB_CLASS]] except KeyError: error_str = f"Missing analysis type for tab {i}. Skipping this tab." self.log.error(error_str) loading_errors.append(error_str) continue # The tab title will be set by _put_settings. index = self.create_new_analysis(tabclass) try: self._put_settings(settings, index) except Exception as ex: error_str = f"Error loading settings for tab {i}: {ex}" self.log.error(error_str) self.log.debug("Backtrace", exc_info=True) loading_errors.append(error_str) self.log.info(f"Completed loading workspace from \"{filename}\"") # 8. if there are any errors, open a dialog with the # complete list of tab errors if loading_errors: QtWidgets.QMessageBox().warning( self, "Errors while loading workspace:", "There were errors while loading the workspace:\n\n{0}". format("\n\n".join(loading_errors))) def save_workspace(self): """Open a file chooser and save the workspace to the chosen file.""" workspace = {} save_errors = [] workspace[SETTINGS_POLICY] = os.path.abspath(str(self.policy)) workspace[SETTINGS_PERMMAP] = os.path.abspath(str(self.permmap)) workspace[SETTINGS_TABS_LIST] = [] for index in range(self.count()): tab = self.widget(index) try: settings = tab.save() except widgets.exception.TabFieldError as ex: tab_name = self.tabText(index) save_errors.append(tab_name) self.log.error(f"Error: tab \"{tab_name}\": {ex}") else: # add the tab info to the settings. settings[SETTINGS_TAB_TITLE] = self.tabText(index) settings[SETTINGS_TAB_CLASS] = type(tab).__name__ workspace[SETTINGS_TABS_LIST].append(settings) if save_errors: self.log.critical("Errors in tabs prevent saving the workspace.") QtWidgets.QMessageBox().critical( self, "Unable to save workspace", "Please resolve errors in the following tabs before saving the " "workspace:\n\n{0}".format('\n'.join(save_errors))) return filename = QtWidgets.QFileDialog.getSaveFileName( self, "Save analysis workspace", "workspace.apolw", "Apol Workspace Files (*.apolw);;" "All Files (*)")[0] if not filename: return with open(filename, "w") as fd: json.dump(workspace, fd, indent=1) # # Edit actions # # @typing.override def copy(self): """Copy text from the currently-focused widget.""" with suppress(Exception): QtWidgets.QApplication.instance().focusWidget().copy() # @typing.override def cut(self): """Cut text from the currently-focused widget.""" with suppress(Exception): QtWidgets.QApplication.instance().focusWidget().cut() # @typing.override def paste(self): """Paste text into the currently-focused widget.""" with suppress(Exception): QtWidgets.QApplication.instance().focusWidget().paste() # # Help actions # def about_apol(self): """Display the about dialog.""" QtWidgets.QMessageBox.about( self, "About Apol", f"""
Apol is a graphical SELinux policy analysis tool and part of SETools.
Copyright (C) 2015-2016, Tresys Technology
Copyright (C) 2016-2023, Chris PeBenito