diff --git a/data/commonquery.ui b/data/commonquery.ui new file mode 100644 index 0000000..fb81025 --- /dev/null +++ b/data/commonquery.ui @@ -0,0 +1,513 @@ + + + CommonQueryTab_ui + + + + 0 + 0 + 774 + 846 + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 772 + 844 + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + 11 + 75 + true + + + + Common Permission Sets + + + + + + + + 0 + 80 + + + + Optionally enter notes here about the query. + + + Enter notes here. + + + + + + + Qt::Horizontal + + + + 440 + 20 + + + + + + + + Show: + + + + + + + Show or hide the search criteria (no settings are lost) + + + Criteria + + + true + + + + + + + Show or hide the notes field (no data is lost) + + + Notes + + + + + + + + 0 + 1 + + + + Qt::Horizontal + + + + Common Browser + + + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Search Criteria + + + + + + Permission Set + + + + + + + 0 + 0 + + + + + 250 + 16777215 + + + + A matching common will have the selected permissions. + + + QAbstractItemView::ExtendedSelection + + + + + + + Clear + + + + + + + A matching common will a permission set equal to the selected permissions. + + + Equal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Invert + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + QDialogButtonBox::Apply + + + + + + + + 16777215 + 120 + + + + Name + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + + 0 + 0 + + + + + 150 + 20 + + + + + 250 + 16777215 + + + + + + + + Use regular expressions to match the role's name. + + + Regex + + + + + + + + + + + + + + 0 + 1 + + + + 0 + + + + + 0 + 0 + + + + Results + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustIgnored + + + true + + + true + + + + + + + + + 0 + 0 + + + + Raw Results + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + Monospace + + + + + + + QPlainTextEdit::NoWrap + + + true + + + + + + + + + + + + + splitter + notes + label + label_2 + horizontalSpacer + criteria_expander + notes_expander + + + + + GetDetailsListView + QListView +
setoolsgui/getdetailslist.h
+
+
+ + criteria_expander + notes_expander + commons + name + name_regex + perms + clear_perms + invert_perms + perms_equal + results_frame + table_results + raw_results + notes + + + + + notes_expander + toggled(bool) + notes + setVisible(bool) + + + 732 + 20 + + + 386 + 754 + + + + + criteria_expander + toggled(bool) + criteria_frame + setVisible(bool) + + + 583 + 20 + + + 496 + 226 + + + + + clear_perms + clicked() + perms + clearSelection() + + + 592 + 216 + + + 442 + 276 + + + + +
diff --git a/setoolsgui/apol/commonquery.py b/setoolsgui/apol/commonquery.py new file mode 100644 index 0000000..156e661 --- /dev/null +++ b/setoolsgui/apol/commonquery.py @@ -0,0 +1,197 @@ +# 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 Qt, QSortFilterProxyModel, QStringListModel, QThread +from PyQt5.QtGui import QPalette, QTextCursor +from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea +from setools import CommonQuery + +from ..logtosignal import LogHandlerToSignal +from ..models import SEToolsListModel, invert_list_selection +from ..commonmodel import CommonTableModel, common_detail +from ..widget import SEToolsWidget +from .queryupdater import QueryResultsUpdater + + +class CommonQueryTab(SEToolsWidget, QScrollArea): + + """Common browser and query tab.""" + + def __init__(self, parent, policy, perm_map): + super(CommonQueryTab, self).__init__(parent) + self.log = logging.getLogger(__name__) + self.policy = policy + self.query = CommonQuery(policy) + self.setupUi() + + def __del__(self): + self.thread.quit() + self.thread.wait(5000) + logging.getLogger("setools.commonquery").removeHandler(self.handler) + + def setupUi(self): + self.load_ui("commonquery.ui") + + # populate commons list + self.common_model = SEToolsListModel(self) + self.common_model.item_list = sorted(c for c in self.policy.commons()) + self.commons.setModel(self.common_model) + + # populate perm list + self.perms_model = SEToolsListModel(self) + perms = set() + for com in self.policy.commons(): + perms.update(com.perms) + self.perms_model.item_list = sorted(perms) + self.perms.setModel(self.perms_model) + + # set up results + self.table_results_model = CommonTableModel(self) + self.sort_proxy = QSortFilterProxyModel(self) + self.sort_proxy.setSourceModel(self.table_results_model) + self.table_results.setModel(self.sort_proxy) + self.table_results.sortByColumn(0, Qt.AscendingOrder) + + # setup indications of errors + 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 = QueryResultsUpdater(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.commonquery").addHandler(self.handler) + + # Ensure settings are consistent with the initial .ui state + self.set_name_regex(self.name_regex.isChecked()) + self.notes.setHidden(not self.notes_expander.isChecked()) + + # connect signals + self.commons.doubleClicked.connect(self.get_detail) + self.commons.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.perms.selectionModel().selectionChanged.connect(self.set_perms) + self.invert_perms.clicked.connect(self.invert_perms_selection) + self.buttonBox.clicked.connect(self.run) + + # + # Class browser + # + def get_detail(self): + # .ui is set for single item selection. + index = self.commons.selectedIndexes()[0] + item = self.common_model.data(index, Qt.UserRole) + + self.log.debug("Generating detail window for {0}".format(item)) + common_detail(self, item) + + # + # Name criteria + # + def clear_name_error(self): + self.name.setToolTip("Match the common 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("Common 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() + + # + # Permissions criteria + # + def set_perms(self): + selected_perms = [] + for index in self.perms.selectionModel().selectedIndexes(): + selected_perms.append(self.perms_model.data(index, Qt.UserRole)) + + self.query.perms = selected_perms + + def invert_perms_selection(self): + invert_list_selection(self.perms.selectionModel()) + + # + # Results runner + # + def run(self, button): + # right now there is only one button. + self.query.perms_equal = self.perms_equal.isChecked() + + # start processing + self.busy.setLabelText("Processing query...") + self.busy.show() + self.raw_results.clear() + self.thread.start() + + def update_complete(self, count): + self.log.info("{0} common(s) found.".format(count)) + + # 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 the permissions column width is too long, pull back + # to a reasonable size + header = self.table_results.horizontalHeader() + if header.sectionSize(1) > 400: + header.resizeSection(1, 400) + + 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() diff --git a/setoolsgui/apol/mainwindow.py b/setoolsgui/apol/mainwindow.py index ddc82ec..1971c46 100644 --- a/setoolsgui/apol/mainwindow.py +++ b/setoolsgui/apol/mainwindow.py @@ -28,6 +28,7 @@ from ..widget import SEToolsWidget from ..logtosignal import LogHandlerToSignal # Analysis tabs: from .boolquery import BoolQueryTab +from .commonquery import CommonQueryTab from .constraintquery import ConstraintQueryTab from .dta import DomainTransitionAnalysisTab from .fsusequery import FSUseQueryTab @@ -279,6 +280,7 @@ class ChooseAnalysis(SEToolsWidget, QDialog): _analysis_map = {"Domain Transition Analysis": DomainTransitionAnalysisTab, "Information Flow Analysis": InfoFlowAnalysisTab} _components_map = {"Booleans": BoolQueryTab, + "Commons": CommonQueryTab, "Roles": RoleQueryTab, "Object Classes": ObjClassQueryTab, "Types": TypeQueryTab, diff --git a/setoolsgui/commonmodel.py b/setoolsgui/commonmodel.py new file mode 100644 index 0000000..cd16497 --- /dev/null +++ b/setoolsgui/commonmodel.py @@ -0,0 +1,67 @@ +# 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 +from PyQt5.QtGui import QPalette, QTextCursor + +from setools.policyrep.exception import NoCommon + +from .details import DetailsPopup +from .models import SEToolsTableModel + + +def common_detail(parent, common): + """ + Create a dialog box for common perm set details. + + Parameters: + parent The parent Qt Widget + class_ The type + """ + + detail = DetailsPopup(parent, "Common detail: {0}".format(common)) + + detail.append_header("Permissions ({0}):".format(len(common.perms))) + for p in sorted(common.perms): + detail.append(" {0}".format(p)) + + detail.show() + + +class CommonTableModel(SEToolsTableModel): + + """Table-based model for common permission sets.""" + + headers = defaultdict(str, {0: "Name", 1: "Permissions"}) + + def data(self, index, role): + if self.resultlist: + row = index.row() + col = index.column() + item = self.resultlist[row] + + if role == Qt.DisplayRole: + if col == 0: + return str(item) + elif col == 1: + return ", ".join(sorted(str(p) for p in item.perms)) + + elif role == Qt.UserRole: + return item