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