diff --git a/setoolsgui/widgets/tab.py b/setoolsgui/widgets/tab.py
index 4a0c36b..161058b 100644
--- a/setoolsgui/widgets/tab.py
+++ b/setoolsgui/widgets/tab.py
@@ -1,9 +1,9 @@
# SPDX-License-Identifier: LGPL-2.1-only
from contextlib import suppress
-from enum import Enum
+import enum
import logging
-from typing import TYPE_CHECKING, cast
+from typing import Generic, TYPE_CHECKING, TypeVar, cast
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -11,11 +11,12 @@ from .exception import TabFieldError
from .models.typing import QObjectType
from .queryupdater import QueryResultsUpdater
from .tableview import SEToolsTableView
+from .treeview import SEToolsTreeWidget
if TYPE_CHECKING:
from typing import Dict, Final, List, Optional, Tuple, Type, Union
from setools import PermissionMap
- from setools.query import PolicyQuery
+ from setools.query import DirectedGraphAnalysis, PolicyQuery
from .criteria.criteria import CriteriaWidget
from .models.table import SEToolsTableModel
@@ -29,8 +30,11 @@ CRITERIA_DEFAULT_CHECKED = True
# Show notes default setting (unchecked)
NOTES_DEFAULT_CHECKED = False
+TAB_REQUIRED_CLASSVARS = ("section", "tab_title", "mlsonly")
+TAB_REGISTRY: "Dict[str, Type[BaseAnalysisTabWidget]]" = {}
-class AnalysisSection(Enum):
+
+class AnalysisSection(enum.Enum):
"""Groupings of analysis tabs"""
@@ -42,10 +46,6 @@ class AnalysisSection(Enum):
Rules = 6
-TAB_REQUIRED_CLASSVARS = ("section", "tab_title", "mlsonly")
-TAB_REGISTRY: "Dict[str, Type[BaseAnalysisTabWidget]]" = {}
-
-
class TabRegistry(QObjectType):
"""
@@ -452,6 +452,173 @@ class TableResultTabWidget(BaseAnalysisTabWidget):
self, "Error", message, QtWidgets.QMessageBox.StandardButton.Ok)
+DGA = TypeVar("DGA", bound="DirectedGraphAnalysis")
+
+
+class DirectedGraphResultTab(BaseAnalysisTabWidget, Generic[DGA]):
+
+ """
+ Application top-level analysis tab that provides a QTabWidget with tabs for results
+ in a graph and in raw text form.
+ """
+
+ # TODO get signals to disable the run button if there are criteria errors.
+
+ class ResultTab(enum.IntEnum):
+
+ """
+ Enumeration of result tabs.
+
+ 0-indexed to match the tab widget indexing.
+ """
+
+ Graph = 0
+ Tree = 1
+ Raw = 2
+
+ def __init__(self, query: DGA, enable_criteria: bool = True,
+ parent: QtWidgets.QWidget | None = None) -> None:
+
+ super().__init__(enable_criteria=enable_criteria, parent=parent)
+ self.query: "Final[DGA]" = query
+
+ # Create tab widget
+ self.results = QtWidgets.QTabWidget(self.top_widget)
+ tw_sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred,
+ QtWidgets.QSizePolicy.Policy.MinimumExpanding)
+ tw_sizePolicy.setHorizontalStretch(0)
+ tw_sizePolicy.setVerticalStretch(2)
+ tw_sizePolicy.setHeightForWidth(self.results.sizePolicy().hasHeightForWidth())
+ self.results.setSizePolicy(tw_sizePolicy)
+
+ #
+ # Create size policy for tabs
+ #
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred,
+ QtWidgets.QSizePolicy.Policy.MinimumExpanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.results.sizePolicy().hasHeightForWidth())
+
+ #
+ # Create placeholder future graphical tab
+ #
+ self.graphical_results = QtWidgets.QWidget(self.results)
+ self.graphical_results.setObjectName("graphical_results")
+ self.graphical_results.setSizePolicy(sizePolicy)
+ self.results.addTab(self.graphical_results, "Graphical Results")
+ self.results.setTabEnabled(DirectedGraphResultTab.ResultTab.Graph, False)
+ self.results.setTabWhatsThis(DirectedGraphResultTab.ResultTab.Graph,
+ "Future graphical results feature.")
+ self.results.setTabToolTip(DirectedGraphResultTab.ResultTab.Graph,
+ "Future graphical results feature.")
+
+ #
+ # Create tree browser tab
+ #
+ self.tree_results = SEToolsTreeWidget(self.results)
+ self.tree_results.setObjectName("tree_results")
+ self.tree_results.setSizePolicy(sizePolicy)
+ self.tree_results.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
+ self.tree_results.setAlternatingRowColors(True)
+ self.tree_results.setSortingEnabled(True)
+ self.tree_results.setWhatsThis(
+ "This tab has the tree-based results of the query.")
+ self.results.addTab(self.tree_results, "Tree Results")
+ self.results.setTabWhatsThis(
+ DirectedGraphResultTab.ResultTab.Tree,
+ "This tab has the tree-based results of the query.")
+
+ #
+ # Create raw result tab
+ #
+ self.raw_results = QtWidgets.QPlainTextEdit(self.results)
+ self.raw_results.setObjectName("raw_results")
+ self.raw_results.setSizePolicy(sizePolicy)
+ self.raw_results.setDocumentTitle("")
+ self.raw_results.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.NoWrap)
+ self.raw_results.setReadOnly(True)
+ self.raw_results.setWhatsThis("This tab has plain text results of the query.")
+ self.results.addTab(self.raw_results, "Raw Results")
+ self.results.setTabWhatsThis(DirectedGraphResultTab.ResultTab.Raw,
+ "This tab has plain text results of the query.")
+
+ # set initial tab
+ self.results.setCurrentIndex(DirectedGraphResultTab.ResultTab.Tree)
+
+ # set up processing thread
+ self.processing_thread = QtCore.QThread(self.top_widget)
+
+ # create a "busy, please wait" dialog
+ self.busy = QtWidgets.QProgressDialog(self.top_widget)
+ self.busy.setModal(True)
+ self.busy.setRange(0, 0)
+ self.busy.setMinimumDuration(0)
+ self.busy.canceled.connect(self.processing_thread.requestInterruption)
+ self.busy.reset()
+
+ # set up results worker
+ self.worker = QueryResultsUpdater[DGA](self.query)
+ self.worker.moveToThread(self.processing_thread)
+ self.worker.raw_line.connect(self.raw_results.appendPlainText)
+ self.worker.finished.connect(self.query_completed)
+ self.worker.finished.connect(self.processing_thread.quit)
+ self.worker.failed.connect(self.query_failed)
+ self.worker.failed.connect(self.processing_thread.quit)
+ self.processing_thread.started.connect(self.worker.update)
+
+ def __del__(self):
+ with suppress(RuntimeError):
+ self.processing_thread.quit()
+ self.processing_thread.wait(5000)
+
+ @property
+ def tree_results_model(self) -> "SEToolsTableModel":
+ return cast("SEToolsTableModel", self.tree_results.model())
+
+ @tree_results_model.setter
+ def tree_results_model(self, model: "SEToolsTableModel") -> None:
+ self.tree_results.setModel(model)
+ self.worker.model = model
+
+ #
+ # Start/end of processing
+ #
+
+ def run(self) -> None:
+ """Start processing query."""
+ errors = [c for c in self.criteria if c.has_errors]
+ if errors:
+ QtWidgets.QMessageBox.critical(
+ self, "Address criteria errors",
+ "Cannot run due to errors in the criteria.",
+ QtWidgets.QMessageBox.StandardButton.Ok)
+ return
+
+ self.busy.setLabelText("Processing query...")
+ self.busy.show()
+ self.raw_results.clear()
+ self.processing_thread.start()
+
+ def query_completed(self, count: int) -> None:
+ """Query completed."""
+ self.log.debug(f"{count} result(s) found.")
+ self.setStatusTip(f"{count} result(s) found.")
+ if not self.busy.wasCanceled():
+ self.busy.setLabelText("Moving the raw result to top; GUI may be unresponsive")
+ self.busy.repaint()
+ self.raw_results.moveCursor(QtGui.QTextCursor.MoveOperation.Start)
+
+ self.busy.reset()
+
+ def query_failed(self, message: str) -> None:
+ self.busy.reset()
+ self.setStatusTip(f"Error: {message}.")
+
+ QtWidgets.QMessageBox.critical(
+ self, "Error", message, QtWidgets.QMessageBox.StandardButton.Ok)
+
+
if __name__ == '__main__':
import sys
import warnings
@@ -461,14 +628,29 @@ if __name__ == '__main__':
format='%(asctime)s|%(levelname)s|%(name)s|%(message)s')
warnings.simplefilter("default")
- q = setools.TERuleQuery(setools.SELinuxPolicy())
+ p = setools.SELinuxPolicy()
+ q = setools.TERuleQuery(p)
+ pmap = setools.PermissionMap()
+ a = setools.InfoFlowAnalysis(p, pmap)
app = QtWidgets.QApplication(sys.argv)
- widget1 = BaseAnalysisTabWidget()
- widget1.show()
- widget2 = BaseAnalysisTabWidget(enable_criteria=False)
- widget2.show()
- widget3 = TableResultTabWidget(q)
- widget3.show()
+ mw = QtWidgets.QMainWindow()
+ whatsthis = QtWidgets.QWhatsThis.createAction(mw)
+ mw.menuBar().addAction(whatsthis)
+ mw.setStatusBar(QtWidgets.QStatusBar(mw))
+
+ tw = QtWidgets.QTabWidget(mw)
+ mw.setCentralWidget(tw)
+ widget1 = BaseAnalysisTabWidget(parent=tw)
+ tw.addTab(widget1, "BaseAnalysisTabWidget w/criteria")
+ widget2 = BaseAnalysisTabWidget(enable_criteria=False, parent=tw)
+ tw.addTab(widget2, "BaseAnalysisTabWidget w/o criteria")
+ widget3 = TableResultTabWidget(q, parent=tw)
+ tw.addTab(widget3, "TableResultTabWidget")
+ widget4 = DirectedGraphResultTab(a, parent=tw)
+ tw.addTab(widget4, "GraphResultTabWidget w/criteria")
+
+ mw.resize(1024, 768)
+ mw.show()
rc = app.exec_()
sys.exit(rc)