diff --git a/setoolsgui/apol/boolquery.py b/setoolsgui/apol/boolquery.py new file mode 100644 index 0000000..c4a86b5 --- /dev/null +++ b/setoolsgui/apol/boolquery.py @@ -0,0 +1,217 @@ +# Copyright 2016, Tresys Technology, LLC +# +# This file is part of SETools. +# +# SETools is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 2.1 of +# the License, or (at your option) any later version. +# +# SETools is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SETools. If not, see +# . +# + +import logging + +from PyQt5.QtCore import pyqtSignal, Qt, QObject, QSortFilterProxyModel, QStringListModel, QThread +from PyQt5.QtGui import QPalette, QTextCursor +from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea +from setools import BoolQuery + +from ..logtosignal import LogHandlerToSignal +from ..models import SEToolsListModel, invert_list_selection +from ..boolmodel import BooleanTableModel, boolean_detail +from ..widget import SEToolsWidget + + +class BoolQueryTab(SEToolsWidget, QScrollArea): + + """Bool browser and query tab.""" + + def __init__(self, parent, policy, perm_map): + super(BoolQueryTab, self).__init__(parent) + self.log = logging.getLogger(__name__) + self.policy = policy + self.query = BoolQuery(policy) + self.setupUi() + + def __del__(self): + self.thread.quit() + self.thread.wait(5000) + logging.getLogger("setools.boolquery").removeHandler(self.handler) + + def setupUi(self): + self.load_ui("boolquery.ui") + + # populate bool list + self.bool_model = SEToolsListModel(self) + self.bool_model.item_list = sorted(r for r in self.policy.bools()) + self.bools.setModel(self.bool_model) + + # set up results + self.table_results_model = BooleanTableModel(self) + self.sort_proxy = QSortFilterProxyModel(self) + self.sort_proxy.setSourceModel(self.table_results_model) + self.table_results.setModel(self.sort_proxy) + + # setup indications of errors on level/range + self.orig_palette = self.name.palette() + self.error_palette = self.name.palette() + self.error_palette.setColor(QPalette.Base, Qt.red) + self.clear_name_error() + + # set up processing thread + self.thread = QThread() + self.worker = ResultsUpdater(self.query, self.table_results_model) + self.worker.moveToThread(self.thread) + self.worker.raw_line.connect(self.raw_results.appendPlainText) + self.worker.finished.connect(self.update_complete) + self.worker.finished.connect(self.thread.quit) + self.thread.started.connect(self.worker.update) + + # create a "busy, please wait" dialog + self.busy = QProgressDialog(self) + self.busy.setModal(True) + self.busy.setRange(0, 0) + self.busy.setMinimumDuration(0) + self.busy.canceled.connect(self.thread.requestInterruption) + self.busy.reset() + + # update busy dialog from query INFO logs + self.handler = LogHandlerToSignal() + self.handler.message.connect(self.busy.setLabelText) + logging.getLogger("setools.boolquery").addHandler(self.handler) + + # Ensure settings are consistent with the initial .ui state + self.notes.setHidden(not self.notes_expander.isChecked()) + + # connect signals + self.bools.doubleClicked.connect(self.get_detail) + self.bools.get_detail.triggered.connect(self.get_detail) + self.name.textEdited.connect(self.clear_name_error) + self.name.editingFinished.connect(self.set_name) + self.name_regex.toggled.connect(self.set_name_regex) + self.buttonBox.clicked.connect(self.run) + + # + # Booleans browser + # + def get_detail(self): + # .ui is set for single item selection. + index = self.bools.selectedIndexes()[0] + item = self.bool_model.data(index, Qt.UserRole) + + self.log.debug("Generating detail window for {0}".format(item)) + boolean_detail(self, item) + + # + # Name criteria + # + def clear_name_error(self): + self.name.setToolTip("Match the Boolean name.") + self.name.setPalette(self.orig_palette) + + def set_name(self): + try: + self.query.name = self.name.text() + except Exception as ex: + self.log.error("Boolean name error: {0}".format(ex)) + self.name.setToolTip("Error: " + str(ex)) + self.name.setPalette(self.error_palette) + + def set_name_regex(self, state): + self.log.debug("Setting name_regex {0}".format(state)) + self.query.name_regex = state + self.clear_name_error() + self.set_name() + + # + # Results runner + # + + def run(self, button): + # right now there is only one button. + if self.default_any.isChecked(): + self.query.default = None + else: + self.query.default = self.default_true.isChecked() + + # start processing + self.busy.setLabelText("Processing query...") + self.busy.show() + self.raw_results.clear() + self.thread.start() + + def update_complete(self): + # update sizes/location of result displays + if not self.busy.wasCanceled(): + self.busy.setLabelText("Resizing the result table's columns; GUI may be unresponsive") + self.busy.repaint() + self.table_results.resizeColumnsToContents() + + if not self.busy.wasCanceled(): + self.busy.setLabelText("Resizing the result table's rows; GUI may be unresponsive") + self.busy.repaint() + self.table_results.resizeRowsToContents() + + 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(QTextCursor.Start) + + self.busy.reset() + + +class ResultsUpdater(QObject): + + """ + Thread for processing queries and updating result widgets. + + Parameters: + query The query object + model The model for the results + + Qt signals: + finished The update has completed. + raw_line (str) A string to be appended to the raw results. + """ + + finished = pyqtSignal() + raw_line = pyqtSignal(str) + + def __init__(self, query, model): + super(ResultsUpdater, self).__init__() + self.query = query + self.log = logging.getLogger(__name__) + self.table_results_model = model + + def update(self): + """Run the query and update results.""" + self.table_results_model.beginResetModel() + + results = [] + counter = 0 + + for counter, item in enumerate(self.query.results(), start=1): + results.append(item) + + self.raw_line.emit(item.statement()) + + if QThread.currentThread().isInterruptionRequested(): + break + elif not counter % 10: + # yield execution every 10 rules + QThread.yieldCurrentThread() + + self.table_results_model.resultlist = results + self.table_results_model.endResetModel() + + self.log.info("{0} Boolean(s) found.".format(counter)) + + self.finished.emit() diff --git a/setoolsgui/apol/mainwindow.py b/setoolsgui/apol/mainwindow.py index 4a9391d..6c3c805 100644 --- a/setoolsgui/apol/mainwindow.py +++ b/setoolsgui/apol/mainwindow.py @@ -27,6 +27,7 @@ from setools import PermissionMap, SELinuxPolicy from ..widget import SEToolsWidget from ..logtosignal import LogHandlerToSignal # Analysis tabs: +from .boolquery import BoolQueryTab from .dta import DomainTransitionAnalysisTab from .infoflow import InfoFlowAnalysisTab from .mlsrulequery import MLSRuleQueryTab @@ -232,7 +233,8 @@ class ChooseAnalysis(SEToolsWidget, QDialog): _analysis_map = {"Domain Transition Analysis": DomainTransitionAnalysisTab, "Information Flow Analysis": InfoFlowAnalysisTab} - _components_map = {"Roles": RoleQueryTab, + _components_map = {"Booleans": BoolQueryTab, + "Roles": RoleQueryTab, "Types": TypeQueryTab, "Users": UserQueryTab} _rule_map = {"RBAC Rules": RBACRuleQueryTab, diff --git a/setoolsgui/boolmodel.py b/setoolsgui/boolmodel.py new file mode 100644 index 0000000..240b51e --- /dev/null +++ b/setoolsgui/boolmodel.py @@ -0,0 +1,80 @@ +# Copyright 2016, Tresys Technology, LLC +# +# This file is part of SETools. +# +# SETools is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 2.1 of +# the License, or (at your option) any later version. +# +# SETools is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SETools. If not, see +# . +# +from collections import defaultdict + +from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex +from PyQt5.QtGui import QPalette, QTextCursor + +from .details import DetailsPopup + + +def boolean_detail(parent, boolean): + """ + Create a dialog box for Booleanean details. + + Parameters: + parent The parent Qt Widget + bool The boolean + """ + + detail = DetailsPopup(parent, "Boolean detail: {0}".format(boolean)) + + detail.append_header("Default State: {0}".format(boolean.state)) + + detail.show() + + +class BooleanTableModel(QAbstractTableModel): + + """Table-based model for booleans.""" + + headers = defaultdict(None, {0: "Name", 1: "Default State"}) + + def __init__(self, parent): + super(BooleanTableModel, self).__init__(parent) + self.resultlist = [] + + def headerData(self, section, orientation, role): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.headers[section] + + def columnCount(self, parent=QModelIndex()): + return 2 + + def rowCount(self, parent=QModelIndex()): + if self.resultlist: + return len(self.resultlist) + else: + return 0 + + def data(self, index, role): + if self.resultlist: + row = index.row() + col = index.column() + boolean = self.resultlist[row] + + if role == Qt.DisplayRole: + if col == 0: + return str(boolean) + elif col == 1: + return str(boolean.state) + + elif role == Qt.UserRole: + # get the whole rule for boolean boolean + return boolean