apol: Add graphical output for Infoflow and DTA.

Signed-off-by: Chris PeBenito <pebenito@ieee.org>
This commit is contained in:
Chris PeBenito 2024-01-29 08:44:24 -05:00 committed by Chris PeBenito
parent ecdc7f460c
commit be79ffa7cd
3 changed files with 123 additions and 20 deletions

View File

@ -6,7 +6,8 @@
import logging import logging
import typing import typing
from PyQt6 import QtCore import networkx as nx
from PyQt6 import QtCore, QtGui, QtWidgets
import setools import setools
from . import models from . import models
@ -42,17 +43,19 @@ class QueryResultsUpdater(QtCore.QObject, typing.Generic[Q]):
finished = QtCore.pyqtSignal(int) finished = QtCore.pyqtSignal(int)
raw_line = QtCore.pyqtSignal(str) raw_line = QtCore.pyqtSignal(str)
def __init__(self, query: Q, def __init__(self, query: Q, /, *,
model: models.SEToolsTableModel | None = None, table_model: models.SEToolsTableModel | None = None,
graphics_buffer: QtWidgets.QLabel | None = None,
render: RenderFunction = lambda _, x: str(x), render: RenderFunction = lambda _, x: str(x),
result_limit: int = 0) -> None: result_limit: int = 0) -> None:
super().__init__() super().__init__()
self.log: typing.Final = logging.getLogger(query.__module__) self.log: typing.Final = logging.getLogger(query.__module__)
self.query: typing.Final[Q] = query self.query: typing.Final[Q] = query
self.model = model self.table_model = table_model
self.render = render self.render = render
self.result_limit = result_limit self.result_limit = result_limit
self.graphics_buffer = graphics_buffer
def update(self) -> None: def update(self) -> None:
"""Run the query and update results.""" """Run the query and update results."""
@ -83,8 +86,19 @@ class QueryResultsUpdater(QtCore.QObject, typing.Generic[Q]):
self.log.info(f"Generated {counter} total results.") self.log.info(f"Generated {counter} total results.")
if self.model: if self.table_model:
self.model.item_list = results self.log.info("Updating results in table model.")
self.table_model.item_list = results
if self.graphics_buffer:
assert isinstance(self.query, setools.query.DirectedGraphAnalysis)
self.log.info("Generating graphical results.")
self.graphics_buffer.clear()
pgv = nx.nx_agraph.to_agraph(self.query.graphical_results())
pic = QtGui.QPixmap()
pic.loadFromData(pgv.draw(prog="dot", format="png"), "PNG")
self.graphics_buffer.setPixmap(pic)
self.finished.emit(counter) self.finished.emit(counter)
except Exception as e: except Exception as e:

View File

@ -8,7 +8,7 @@ import typing
from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import QtCore, QtGui, QtWidgets
import setools import setools
from . import criteria, exception, models, views from . import criteria, exception, models, util, views
from .models.typing import QObjectType from .models.typing import QObjectType
from .queryupdater import QueryResultsUpdater from .queryupdater import QueryResultsUpdater
@ -462,7 +462,7 @@ class TableResultTabWidget(BaseAnalysisTabWidget):
"""Set the table results model for this tab and set up the processing thread for it.""" """Set the table results model for this tab and set up the processing thread for it."""
self.sort_proxy.setSourceModel(model) self.sort_proxy.setSourceModel(model)
self.worker = QueryResultsUpdater(self.query, model) self.worker = QueryResultsUpdater(self.query, table_model=model)
self.worker.moveToThread(self.processing_thread) self.worker.moveToThread(self.processing_thread)
self.worker.raw_line.connect(self.raw_results.appendPlainText) self.worker.raw_line.connect(self.raw_results.appendPlainText)
self.worker.finished.connect(self.query_completed) self.worker.finished.connect(self.query_completed)
@ -583,17 +583,27 @@ class DirectedGraphResultTab(BaseAnalysisTabWidget, typing.Generic[DGA]):
sizePolicy.setHeightForWidth(self.results.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.results.sizePolicy().hasHeightForWidth())
# #
# Create placeholder future graphical tab # Create graphical tab
# #
self.graphical_results = QtWidgets.QWidget(self.results) self.graphical_scroll = QtWidgets.QScrollArea(self.results)
self.graphical_scroll.setSizeAdjustPolicy(
QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents)
self.graphical_scroll.setWidgetResizable(True)
self.results.addTab(self.graphical_scroll, "Graphical Results")
image_size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding,
QtWidgets.QSizePolicy.Policy.MinimumExpanding)
image_size_policy.setHorizontalStretch(1)
image_size_policy.setVerticalStretch(1)
image_size_policy.setHeightForWidth(True)
self.graphical_results = QtWidgets.QLabel(self.graphical_scroll)
self.graphical_results.setObjectName("graphical_results") self.graphical_results.setObjectName("graphical_results")
self.graphical_results.setSizePolicy(sizePolicy) self.graphical_results.setSizePolicy(image_size_policy)
self.results.addTab(self.graphical_results, "Graphical Results") self.graphical_results.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
self.results.setTabEnabled(DirectedGraphResultTab.ResultTab.Graph, False) self.graphical_results.customContextMenuRequested.connect(
self.results.setTabWhatsThis(DirectedGraphResultTab.ResultTab.Graph, self._graphical_results_context_menu)
"Future graphical results feature.") self.graphical_scroll.setWidget(self.graphical_results)
self.results.setTabToolTip(DirectedGraphResultTab.ResultTab.Graph,
"Future graphical results feature.")
# #
# Create tree browser tab # Create tree browser tab
@ -627,7 +637,7 @@ class DirectedGraphResultTab(BaseAnalysisTabWidget, typing.Generic[DGA]):
"<b>This tab has plain text results of the query.</b>") "<b>This tab has plain text results of the query.</b>")
# set initial tab # set initial tab
self.results.setCurrentIndex(DirectedGraphResultTab.ResultTab.Tree) self.results.setCurrentIndex(DirectedGraphResultTab.ResultTab.Graph)
# set up processing thread # set up processing thread
self.processing_thread = QtCore.QThread(self.analysis_widget) self.processing_thread = QtCore.QThread(self.analysis_widget)
@ -641,7 +651,7 @@ class DirectedGraphResultTab(BaseAnalysisTabWidget, typing.Generic[DGA]):
self.busy.reset() self.busy.reset()
# set up results worker # set up results worker
self.worker = QueryResultsUpdater[DGA](self.query) self.worker = QueryResultsUpdater[DGA](self.query, graphics_buffer=self.graphical_results)
self.worker.moveToThread(self.processing_thread) self.worker.moveToThread(self.processing_thread)
self.worker.raw_line.connect(self.raw_results.appendPlainText) self.worker.raw_line.connect(self.raw_results.appendPlainText)
self.worker.finished.connect(self.query_completed) self.worker.finished.connect(self.query_completed)
@ -662,7 +672,33 @@ class DirectedGraphResultTab(BaseAnalysisTabWidget, typing.Generic[DGA]):
@tree_results_model.setter @tree_results_model.setter
def tree_results_model(self, model: models.SEToolsTableModel) -> None: def tree_results_model(self, model: models.SEToolsTableModel) -> None:
self.tree_results.setModel(model) self.tree_results.setModel(model)
self.worker.model = model self.worker.table_model = model
def _graphical_results_context_menu(self, pos: QtCore.QPoint) -> None:
"""Generate context menu for graphical results widget."""
save_action = QtGui.QAction("Save As...", self.graphical_results)
save_action.triggered.connect(self._save_graphical_results)
menu = QtWidgets.QMenu(self.graphical_results)
menu.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
menu.addActions((save_action,))
menu.exec(self.graphical_results.mapToGlobal(pos))
def _save_graphical_results(self) -> None:
"""Save the graphical results to a file."""
with util.QMessageOnException("Error",
"<b>Failed to save graphical results.</b>",
log=self.log,
parent=self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Save graphical results", "", "PNG files (*.png);;All files (*)")
if filename:
if not self.graphical_results.pixmap().save(filename, format="PNG"):
# The save method does not raise an exception, so unfortunately
# there is no additional info to share with the user.
raise RuntimeError(f"Failed to save graphical results to {filename}.")
# #
# Start/end of processing # Start/end of processing

View File

@ -0,0 +1,53 @@
# SPDX-License-Identifier: LGPL-2.1-only
import logging
import traceback
import types
import typing
from PyQt6 import QtWidgets
__all__: typing.Final[tuple[str, ...]] = ("QMessageOnException",)
class QMessageOnException:
"""Context manager to display a message box on exception."""
def __init__(self, title: str, message: str, /, *,
suppress: bool = True,
log: logging.Logger | None = None,
icon: QtWidgets.QMessageBox.Icon = QtWidgets.QMessageBox.Icon.Critical,
parent: QtWidgets.QWidget | None = None) -> None:
self.title: typing.Final[str] = title
self.message: typing.Final[str] = message
self.suppress: typing.Final[bool] = suppress
self.parent: typing.Final[QtWidgets.QWidget | None] = parent
self.log: typing.Final[logging.Logger] = log if log else logging.getLogger(__name__)
self.icon: typing.Final[QtWidgets.QMessageBox.Icon] = icon
def __enter__(self) -> None:
pass
def __exit__(self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
tb: types.TracebackType | None) -> bool:
if exc_type:
self.log.critical(self.message)
self.log.debug("Backtrace", exc_info=True)
msg = QtWidgets.QMessageBox(self.icon,
self.title,
self.message,
parent=self.parent)
msg.setInformativeText(str(exc_value))
msg.setDetailedText("\n".join(traceback.format_tb(tb)))
msg.exec()
return self.suppress
return False