diff --git a/data/netifconquery.ui b/data/netifconquery.ui new file mode 100644 index 0000000..6c6270b --- /dev/null +++ b/data/netifconquery.ui @@ -0,0 +1,706 @@ + + + NetifconQueryTab_ui + + + + 0 + 0 + 774 + 846 + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 772 + 844 + + + + + 0 + 0 + + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + + 0 + 0 + + + + + 0 + 80 + + + + Optionally enter notes here about the query. + + + Enter notes here. + + + + + + + Show or hide the notes field (no data is lost) + + + Notes + + + + + + + Show or hide the search criteria (no settings are lost) + + + Criteria + + + true + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + 12 + 75 + true + + + + Netifcon Statements + + + + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 16777215 + 16777215 + + + + Device Context Range + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + Match the context's range if the criteria is equal to the context's range. + + + Equal + + + true + + + + + + + Match the context's range if the criteria overlaps the context's range. + + + Overlap + + + + + + + Match the context's range if the criteria is a subset of the context's range. + + + Subset + + + + + + + Match the context's range if the criteria is a superset to the context's range. + + + Superset + + + + + + + + 0 + 0 + + + + + 150 + 20 + + + + + 250 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QDialogButtonBox::Apply + + + + + + + + 16777215 + 100 + + + + Device Context Type + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + Use regular expressions to match the context's type. + + + Regex + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 250 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 16777215 + 100 + + + + Device Context Role + + + + 6 + + + 6 + + + 6 + + + 6 + + + 3 + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 250 + 16777215 + + + + + + + + true + + + Use regular expressions to match the context's role. + + + Regex + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Device Context User + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + + + + Use regular expressions to match the context's user. + + + Regex + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Device Name + + + + + + Use regular expressions to match the filesystem type. + + + Regex + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + 250 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + role_criteria + type_criteria + user_critera + range_criteria + buttonBox + name_criteria + + + + + + Show: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 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 + + + + + + + + + + + + criteria_expander + notes_expander + name + name_regex + user + user_regex + role + role_regex + type_ + type_regex + range_ + range_exact + range_overlap + range_subset + range_superset + results_frame + table_results + raw_results + notes + + + + + criteria_expander + toggled(bool) + criteria_frame + setVisible(bool) + + + 592 + 16 + + + 386 + 232 + + + + + notes_expander + toggled(bool) + notes + setVisible(bool) + + + 735 + 16 + + + 386 + 756 + + + + + diff --git a/setoolsgui/apol/mainwindow.py b/setoolsgui/apol/mainwindow.py index 4e7d822..c3e3841 100644 --- a/setoolsgui/apol/mainwindow.py +++ b/setoolsgui/apol/mainwindow.py @@ -34,6 +34,7 @@ from .genfsconquery import GenfsconQueryTab from .infoflow import InfoFlowAnalysisTab from .initsidquery import InitialSIDQueryTab from .mlsrulequery import MLSRuleQueryTab +from .netifconquery import NetifconQueryTab from .rbacrulequery import RBACRuleQueryTab from .rolequery import RoleQueryTab from .terulequery import TERuleQueryTab @@ -282,7 +283,8 @@ class ChooseAnalysis(SEToolsWidget, QDialog): "TE Rules": TERuleQueryTab} _labeling_map = {"fs_use_* Statements": FSUseQueryTab, "Genfscon Statements": GenfsconQueryTab, - "Initial SID Statements": InitialSIDQueryTab} + "Initial SID Statements": InitialSIDQueryTab, + "Netifcon Statements": NetifconQueryTab} _analysis_choices = {"Components": _components_map, "Rules": _rule_map, "Analyses": _analysis_map, diff --git a/setoolsgui/apol/netifconquery.py b/setoolsgui/apol/netifconquery.py new file mode 100644 index 0000000..cdd070a --- /dev/null +++ b/setoolsgui/apol/netifconquery.py @@ -0,0 +1,273 @@ +# 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 NetifconQuery + +from ..logtosignal import LogHandlerToSignal +from ..netifconmodel import NetifconTableModel +from ..widget import SEToolsWidget +from .queryupdater import QueryResultsUpdater + + +class NetifconQueryTab(SEToolsWidget, QScrollArea): + + """A netifcon query.""" + + def __init__(self, parent, policy, perm_map): + super(NetifconQueryTab, self).__init__(parent) + self.log = logging.getLogger(__name__) + self.policy = policy + self.query = NetifconQuery(policy) + self.setupUi() + + def __del__(self): + self.thread.quit() + self.thread.wait(5000) + logging.getLogger("setools.netifconquery").removeHandler(self.handler) + + def setupUi(self): + self.load_ui("netifconquery.ui") + + # set up user autocompletion + user_completion_list = [str(u) for u in self.policy.users()] + user_completer_model = QStringListModel(self) + user_completer_model.setStringList(sorted(user_completion_list)) + self.user_completion = QCompleter() + self.user_completion.setModel(user_completer_model) + self.user.setCompleter(self.user_completion) + + # set up role autocompletion + role_completion_list = [str(r) for r in self.policy.roles()] + role_completer_model = QStringListModel(self) + role_completer_model.setStringList(sorted(role_completion_list)) + self.role_completion = QCompleter() + self.role_completion.setModel(role_completer_model) + self.role.setCompleter(self.role_completion) + + # set up type autocompletion + type_completion_list = [str(t) for t in self.policy.types()] + type_completer_model = QStringListModel(self) + type_completer_model.setStringList(sorted(type_completion_list)) + self.type_completion = QCompleter() + self.type_completion.setModel(type_completer_model) + self.type_.setCompleter(self.type_completion) + + # setup indications of errors on source/target/default + self.orig_palette = self.type_.palette() + self.error_palette = self.type_.palette() + self.error_palette.setColor(QPalette.Base, Qt.red) + self.clear_name_error() + self.clear_user_error() + self.clear_type_error() + self.clear_role_error() + self.clear_range_error() + + # set up results + self.table_results_model = NetifconTableModel(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) + + # 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.netifconquery").addHandler(self.handler) + + # Ensure settings are consistent with the initial .ui state + self.set_name_regex(self.name_regex.isChecked()) + self.criteria_frame.setHidden(not self.criteria_expander.isChecked()) + self.notes.setHidden(not self.notes_expander.isChecked()) + + # Range criteria is visible only if policy is MLS + self.range_criteria.setVisible(self.policy.mls) + + # connect signals + self.buttonBox.clicked.connect(self.run) + 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.user.textEdited.connect(self.clear_user_error) + self.user.editingFinished.connect(self.set_user) + self.user_regex.toggled.connect(self.set_user_regex) + self.role.textEdited.connect(self.clear_role_error) + self.role.editingFinished.connect(self.set_role) + self.role_regex.toggled.connect(self.set_role_regex) + self.type_.textEdited.connect(self.clear_type_error) + self.type_.editingFinished.connect(self.set_type) + self.type_regex.toggled.connect(self.set_type_regex) + self.range_.textEdited.connect(self.clear_range_error) + self.range_.editingFinished.connect(self.set_range) + + # + # Name criteria + # + def clear_name_error(self): + self.name.setToolTip("Match the device 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("Device 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() + + # + # User criteria + # + def clear_user_error(self): + self.user.setToolTip("Match the user of the context.") + self.user.setPalette(self.orig_palette) + + def set_user(self): + try: + self.query.user = self.user.text() + except Exception as ex: + self.log.error("Context user error: {0}".format(ex)) + self.user.setToolTip("Error: " + str(ex)) + self.user.setPalette(self.error_palette) + + def set_user_regex(self, state): + self.log.debug("Setting user_regex {0}".format(state)) + self.query.user_regex = state + self.clear_user_error() + self.set_user() + + # + # Role criteria + # + def clear_role_error(self): + self.role.setToolTip("Match the role of the context.") + self.role.setPalette(self.orig_palette) + + def set_role(self): + try: + self.query.role = self.role.text() + except Exception as ex: + self.log.error("Context role error: {0}".format(ex)) + self.role.setToolTip("Error: " + str(ex)) + self.role.setPalette(self.error_palette) + + def set_role_regex(self, state): + self.log.debug("Setting role_regex {0}".format(state)) + self.query.role_regex = state + self.clear_role_error() + self.set_role() + + # + # Type criteria + # + def clear_type_error(self): + self.type_.setToolTip("Match the type of the context.") + self.type_.setPalette(self.orig_palette) + + def set_type(self): + try: + self.query.type_ = self.type_.text() + except Exception as ex: + self.log.error("Context type error: {0}".format(ex)) + self.type_.setToolTip("Error: " + str(ex)) + self.type_.setPalette(self.error_palette) + + def set_type_regex(self, state): + self.log.debug("Setting type_regex {0}".format(state)) + self.query.type_regex = state + self.clear_type_error() + self.set_type() + + # + # Range criteria + # + def clear_range_error(self): + self.range_.setToolTip("Match the range of the context.") + self.range_.setPalette(self.orig_palette) + + def set_range(self): + try: + self.query.range_ = self.range_.text() + except Exception as ex: + self.log.info("Context range error: " + str(ex)) + self.range_.setToolTip("Error: " + str(ex)) + self.range_.setPalette(self.error_palette) + + # + # Results runner + # + def run(self, button): + # right now there is only one button. + self.query.range_overlap = self.range_overlap.isChecked() + self.query.range_subset = self.range_subset.isChecked() + self.query.range_superset = self.range_superset.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} initial SID statment(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 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/netifconmodel.py b/setoolsgui/netifconmodel.py new file mode 100644 index 0000000..1fcdf5e --- /dev/null +++ b/setoolsgui/netifconmodel.py @@ -0,0 +1,50 @@ +# 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, QModelIndex + +from .models import SEToolsTableModel + + +class NetifconTableModel(SEToolsTableModel): + + """Table-based model for netifcons.""" + + headers = defaultdict(str, {0: "Device", 1: "Device Context", 2: "Packet Context"}) + + def columnCount(self, parent=QModelIndex()): + return 3 + + def data(self, index, role): + if self.resultlist: + row = index.row() + col = index.column() + rule = self.resultlist[row] + + if role == Qt.DisplayRole: + if col == 0: + return str(rule.netif) + elif col == 1: + return str(rule.context) + elif col == 2: + return str(rule.packet) + + elif role == Qt.UserRole: + return rule