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