# SPDX-License-Identifier: LGPL-2.1-only from contextlib import suppress import typing from PyQt6 import QtCore, QtWidgets import setools from . import criteria, tab from .excludetypes import ExcludeTypes from .permmap import PermissionMapEditor DEFAULT_ALL_PATHS_STEPS: typing.Final[int] = 3 MIN_ALL_PATHS_STEPS: typing.Final[int] = 1 DEFAULT_MIN_PERM_WT: typing.Final[int] = 3 MIN_MIN_PERM_WT: typing.Final[int] = setools.PermissionMap.MIN_WEIGHT MAX_MIN_PERM_WT: typing.Final[int] = setools.PermissionMap.MAX_WEIGHT DEFAULT_RESULT_LIMIT: typing.Final[int] = 20 MIN_RESULT_LIMIT: typing.Final[int] = 1 SETTINGS_SOURCE: typing.Final[str] = "source" SETTINGS_TARGET: typing.Final[str] = "target" SETTINGS_MODE: typing.Final[str] = "mode" SETTINGS_MIN_WEIGHT: typing.Final[str] = "min_weight" SETTINGS_RESULT_LIMIT: typing.Final[str] = "result_limit" SETTINGS_ALL_PATHS_STEPS: typing.Final[str] = "all_paths_steps" SETTINGS_EXCLUDE_TYPES: typing.Final[str] = "exclude_types" class InfoFlowAnalysisTab(tab.DirectedGraphResultTab[setools.InfoFlowAnalysis]): """An information flow analysis.""" section = tab.AnalysisSection.Analysis tab_title = "Information Flow Analysis" mlsonly = False def __init__(self, policy: setools.SELinuxPolicy, perm_map: setools.PermissionMap, /, *, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(setools.InfoFlowAnalysis(policy, perm_map), perm_map, enable_criteria=True, parent=parent) self.setWhatsThis("Information flow analysis of an SELinux policy.") # # Set up criteria widgets # src = criteria.TypeOrAttrNameWidget("Source Type", self.query, SETTINGS_SOURCE, mode=criteria.TypeOrAttrNameWidget.Mode.type_only, enable_regex=False, enable_indirect=False, required=True, parent=self.criteria_frame) src.setToolTip("The source type of the analysis.") src.setWhatsThis( """

For shortest path and all paths analyses, this this is the source type of the analysis.

For flows out analysis, the analysis will return the information flows out of this type.

This is not used for flows in analysis. """) # # Configure mode frame # modeframe = InfoFlowMode(self.query, parent=self) modeframe.selectionChanged.connect(self._apply_mode_change) # # Configure target type # dst = criteria.TypeOrAttrNameWidget("Target Type", self.query, SETTINGS_TARGET, mode=criteria.TypeOrAttrNameWidget.Mode.type_only, enable_regex=False, enable_indirect=False, required=True, parent=self.criteria_frame) dst.setToolTip("The target type of the analysis.") dst.setWhatsThis( """

For shortest path and all paths analyses, this this is the target type of the analysis.

This is not used for flows out analysis.

For flows in analysis, the analysis will return the information flows into this type.

""") # # Configure options frame # optframe = InfoFlowOptions(self.query, parent=self.criteria_frame) optframe.result_limit_changed.connect(self._apply_result_limit) self._apply_result_limit() # # Set up tree view # # Disable source/target criteria based on info flow in/out modeframe.criteria[setools.InfoFlowAnalysis.Mode.FlowsOut].toggled.connect(dst.setDisabled) modeframe.criteria[setools.InfoFlowAnalysis.Mode.FlowsIn].toggled.connect(src.setDisabled) # # Final setup # # ensure the mode is properly reflected self._apply_mode_change(self.query.mode) # Add widgets to layout self.criteria_frame_layout.addWidget(src, 1, 0, 2, 1) self.criteria_frame_layout.addWidget(modeframe, 0, 1, 2, 1) self.criteria_frame_layout.addWidget(dst, 1, 2, 2, 1) self.criteria_frame_layout.addWidget(optframe, 2, 1, 2, 1) self.criteria_frame_layout.addWidget(self.buttonBox, 4, 0, 1, 3) # Save widget references self.criteria = (src, dst, modeframe, optframe) # Set result table's model # self.tree_results_model = models.MLSRuleTable(self.table_results) def _apply_mode_change(self, mode: setools.InfoFlowAnalysis.Mode) -> None: """Reconfigure after an analysis mode change.""" # Only enable tree browser for flows in/out mode. Set the correct # renderer based on the mode. self.log.debug(f"Handling mode change to {mode}.") results = typing.cast(QtWidgets.QTabWidget, self.results) if mode in (setools.InfoFlowAnalysis.Mode.FlowsIn, setools.InfoFlowAnalysis.Mode.FlowsOut): results.setTabEnabled(tab.DirectedGraphResultTab.ResultTab.Tree, True) self.worker.render = InfoFlowAnalysisTab.render_direct_path else: results.setTabEnabled(tab.DirectedGraphResultTab.ResultTab.Tree, False) self.worker.render = InfoFlowAnalysisTab.render_transitive_path def _apply_result_limit(self, value: int = DEFAULT_RESULT_LIMIT) -> None: """Apply result limit change.""" assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing self.log.debug(f"Setting result limit to {value} flows.") self.worker.result_limit = value def handle_permmap_change(self, permmap: setools.PermissionMap) -> None: self.log.debug(f"Applying updated permission map {permmap}") self.query.perm_map = permmap @staticmethod def render_direct_path(count: int, step: setools.InfoFlowStep) -> str: """Render text representation of flows in/out results.""" return f"Flow {count}: {step:full}\n" @staticmethod def render_transitive_path(count: int, path: setools.InfoFlowPath) -> str: """Render text representation of all/shortest paths results.""" lines = [f"Flow {count}:"] for stepnum, step in enumerate(path, start=1): lines.append(f" Step {stepnum}: {step:full}\n") return "\n".join(lines) class InfoFlowMode(criteria.RadioEnumCriteria[setools.InfoFlowAnalysis.Mode]): """Information flow analysis mode radio buttons.""" def __init__(self, query: setools.InfoFlowAnalysis, parent: QtWidgets.QWidget | None = None) -> None: # colspan 2 so the below spinbox fits better with the radio button. super().__init__("Analysis Mode", query, SETTINGS_MODE, setools.InfoFlowAnalysis.Mode, colspan=2, parent=parent) # Add all paths steps to mode widget. self.all_path_steps = QtWidgets.QSpinBox(self) self.all_path_steps.valueChanged.connect(self._apply_all_path_steps) self.all_path_steps.setSuffix(" steps") self.all_path_steps.setMinimum(MIN_ALL_PATHS_STEPS) self.all_path_steps.setValue(DEFAULT_ALL_PATHS_STEPS) # get layout location of all paths option all_path_index = self.top_layout.indexOf( self.criteria[setools.InfoFlowAnalysis.Mode.AllPaths]) row, col, _, _ = self.top_layout.getItemPosition(all_path_index) assert row is not None and col is not None, \ "Layout position is None, this is an SETools bug." # type narrowing assert row >= 0 and col >= 0, \ f"Invalid layout position, this is an SETools bug. ({row},{col})" # add steps spin box in the next column of the radio button self.top_layout.addWidget(self.all_path_steps, row, col + 1, 1, 1) # set path steps to enable only if the corresponding mode is selected. # it starts disabled since shortest paths is the default option. self.all_path_steps.setEnabled(False) self.criteria[setools.InfoFlowAnalysis.Mode.AllPaths].toggled.connect( self.all_path_steps.setEnabled) def _apply_all_path_steps(self, value: int = DEFAULT_ALL_PATHS_STEPS) -> None: """Apply the value of the all paths spinbox to the query.""" assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing self.log.debug(f"All paths max steps to {value} steps.") self.query.all_paths_max_steps = value def save(self, settings: dict) -> None: super().save(settings) settings[SETTINGS_ALL_PATHS_STEPS] = self.all_path_steps.value() def load(self, settings: dict) -> None: with suppress(KeyError): self.all_path_steps.setValue(settings[SETTINGS_ALL_PATHS_STEPS]) super().load(settings) class InfoFlowOptions(criteria.CriteriaWidget): """ Infoflow analysis options widget. Presents the options: * Minimum permission weight * Limit number of results * Exclude types button * Exclude permissions/classes button """ has_errors: typing.Final[bool] = False result_limit_changed = QtCore.pyqtSignal(int) def __init__(self, query: setools.InfoFlowAnalysis, parent: QtWidgets.QWidget | None = None) -> None: super().__init__("Options", query, "", parent=parent) self.top_layout = QtWidgets.QFormLayout(self) self.top_layout.setLabelAlignment(QtCore.Qt.AlignmentFlag.AlignRight) self.min_weight = QtWidgets.QSpinBox(self) self.min_weight.valueChanged.connect(self._apply_min_weight) self.min_weight.setValue(DEFAULT_MIN_PERM_WT) self.min_weight.setMinimum(MIN_MIN_PERM_WT) self.min_weight.setMaximum(MAX_MIN_PERM_WT) self.top_layout.addRow(QtWidgets.QLabel("Minimum permission weight:", self), self.min_weight) self.result_limit = QtWidgets.QSpinBox(self) self.result_limit.valueChanged.connect(self.result_limit_changed) self.result_limit.setValue(DEFAULT_RESULT_LIMIT) self.result_limit.setMinimum(MIN_RESULT_LIMIT) self.top_layout.addRow(QtWidgets.QLabel("Limit results:", self), self.result_limit) self.edit_excluded_types = QtWidgets.QPushButton("Edit...", self) self.edit_excluded_types.clicked.connect(self._start_type_exclude) self.top_layout.addRow(QtWidgets.QLabel("Excluded types:", self), self.edit_excluded_types) self.edit_excluded_perms = QtWidgets.QPushButton("Edit...", self) self.edit_excluded_perms.clicked.connect(self._start_permmap_exclude) self.top_layout.addRow(QtWidgets.QLabel("Excluded permissions:", self), self.edit_excluded_perms) def _apply_min_weight(self, value: int) -> None: """Apply minimum perm weight to the query.""" assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing self.log.debug(f"Setting min permission weight to {value}") self.query.min_weight = value def _apply_permmap(self, new_map: setools.PermissionMap) -> None: assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing self.log.debug("Applying updated permission map.") self.query.perm_map = new_map def _start_permmap_exclude(self) -> None: w = PermissionMapEditor(self.query.perm_map, edit=False, parent=self) w.apply_permmap.connect(self._apply_permmap) w.open() def _start_type_exclude(self) -> None: ExcludeTypes(self.query, parent=self).open() def save(self, settings: dict) -> None: assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing super().save(settings) settings[SETTINGS_MIN_WEIGHT] = self.min_weight.value() settings[SETTINGS_RESULT_LIMIT] = self.result_limit.value() settings[SETTINGS_EXCLUDE_TYPES] = [str(t) for t in self.query.exclude] # TODO: permmap with enable/disable states def load(self, settings: dict) -> None: assert isinstance(self.query, setools.InfoFlowAnalysis) # type narrowing with suppress(KeyError): self.min_weight.setValue(settings[SETTINGS_MIN_WEIGHT]) with suppress(KeyError): self.result_limit.setValue(settings[SETTINGS_RESULT_LIMIT]) with suppress(KeyError): self.query.exclude = settings[SETTINGS_EXCLUDE_TYPES] # TODO: perm map super().load(settings) if __name__ == '__main__': import sys import logging import pprint import warnings logging.basicConfig(level=logging.DEBUG, format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') warnings.simplefilter("default") app = QtWidgets.QApplication(sys.argv) mw = QtWidgets.QMainWindow() widget = InfoFlowAnalysisTab(setools.SELinuxPolicy(), setools.PermissionMap(), parent=mw) mw.setCentralWidget(widget) mw.resize(widget.size()) whatsthis = QtWidgets.QWhatsThis.createAction(mw) mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] mw.setStatusBar(QtWidgets.QStatusBar(mw)) mw.resize(1024, 768) mw.show() rc = app.exec() pprint.pprint(widget.save()) sys.exit(rc)