diff --git a/data/dta.ui b/data/dta.ui index b7d993e..57efcdc 100644 --- a/data/dta.ui +++ b/data/dta.ui @@ -464,6 +464,16 @@ + + + + Show or hide the notes field (no data is lost) + + + Notes + + + @@ -472,6 +482,9 @@ 1 + + 0 + @@ -530,16 +543,46 @@ - - - - - - Show or hide the notes field (no data is lost) - - - Notes - + + + Browser + + + + + + Qt::Horizontal + + + + + 1 + 0 + + + + + Type + + + + + + + 4 + 0 + + + + + Monospace + + + + + + + diff --git a/data/infoflow.ui b/data/infoflow.ui index 57b3819..dde62ba 100644 --- a/data/infoflow.ui +++ b/data/infoflow.ui @@ -503,6 +503,49 @@ + + + true + + + Browser + + + + + + Qt::Horizontal + + + + + 1 + 0 + + + + + Type + + + + + + + 4 + 0 + + + + + Monospace + + + + + + + diff --git a/setoolsgui/apol/dta.py b/setoolsgui/apol/dta.py index 2cd7a77..759d660 100644 --- a/setoolsgui/apol/dta.py +++ b/setoolsgui/apol/dta.py @@ -19,9 +19,10 @@ import logging -from PyQt5.QtCore import pyqtSignal, Qt, QObject, QStringListModel, QThread +from PyQt5.QtCore import pyqtSignal, Qt, QStringListModel, QThread from PyQt5.QtGui import QPalette, QTextCursor -from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea +from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea, \ + QTreeWidgetItem from setools import DomainTransitionAnalysis from ..logtosignal import LogHandlerToSignal @@ -68,13 +69,14 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.clear_target_error() # set up processing thread - self.thread = QThread() - self.worker = ResultsUpdater(self.query) - 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) + self.thread = ResultsUpdater(self.query) + self.thread.raw_line.connect(self.raw_results.appendPlainText) + self.thread.finished.connect(self.update_complete) + self.thread.trans.connect(self.reset_browser) + + # set up browser thread + self.browser_thread = BrowserUpdater(self.query) + self.browser_thread.trans.connect(self.add_browser_children) # create a "busy, please wait" dialog self.busy = QProgressDialog(self) @@ -95,6 +97,7 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.target.setEnabled(not self.flows_out.isChecked()) self.criteria_frame.setHidden(not self.criteria_expander.isChecked()) self.notes.setHidden(not self.notes_expander.isChecked()) + self.browser_tab.setEnabled(self.flows_in.isChecked() or self.flows_out.isChecked()) # connect signals self.buttonBox.clicked.connect(self.run) @@ -107,6 +110,7 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.flows_out.toggled.connect(self.flows_out_toggled) self.reverse.stateChanged.connect(self.reverse_toggled) self.exclude_types.clicked.connect(self.choose_excluded_types) + self.browser.currentItemChanged.connect(self.browser_item_selected) # # Analysis mode @@ -121,6 +125,8 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.clear_target_error() self.source.setEnabled(not value) self.reverse.setEnabled(not value) + self.limit_paths.setEnabled(not value) + self.browser_tab.setEnabled(value) if value: self.reverse_old = self.reverse.isChecked() @@ -133,6 +139,8 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.clear_target_error() self.target.setEnabled(not value) self.reverse.setEnabled(not value) + self.limit_paths.setEnabled(not value) + self.browser_tab.setEnabled(value) if value: self.reverse_old = self.reverse.isChecked() @@ -196,10 +204,91 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): def reverse_toggled(self, value): self.query.reverse = value + # + # Infoflow browser + # + def _new_browser_item(self, type_, parent, rules=None, children=None): + # build main item + item = QTreeWidgetItem(parent if parent else self.browser) + item.setText(0, str(type_)) + item.type_ = type_ + item.children = children if children else [] + item.rules = rules if rules else [] + item.child_populated = children is not None + + # add child items + for child_type, child_rules in item.children: + child_item = self._new_browser_item(child_type, item, rules=child_rules) + item.addChild(child_item) + + item.setExpanded(children is not None) + + self.log.debug("Built item for {0} with {1} children and {2} rules".format( + type_, len(item.children), len(item.rules))) + + return item + + def reset_browser(self, root_type, out, children): + self.log.debug("Resetting browser.") + + # clear results + self.browser.clear() + self.browser_details.clear() + + # save browser details independent + # from main analysis UI settings + self.browser_root_type = root_type + self.browser_mode = out + + root = self._new_browser_item(self.browser_root_type, self.browser, children=children) + + self.browser.insertTopLevelItem(0, root) + + def browser_item_selected(self, current, previous): + if not current: + # browser is being reset + return + + self.log.debug("{0} selected in browser.".format(current.type_)) + self.browser_details.clear() + + try: + parent_type = current.parent().type_ + except AttributeError: + # should only hit his on the root item + pass + else: + self.browser_details.appendPlainText("Domain Transitions {0} {1} {2}\n".format( + current.parent().type_, "->" if self.browser_mode else "<-", current.type_)) + + print_transition(self.browser_details.appendPlainText, current.rules) + + self.browser_details.moveCursor(QTextCursor.Start) + + if not current.child_populated: + self.busy.setLabelText("Gathering additional browser details for {0}...".format( + current.type_)) + self.busy.show() + self.browser_thread.out = self.browser_mode + self.browser_thread.type_ = current.type_ + self.browser_thread.start() + + def add_browser_children(self, children): + item = self.browser.currentItem() + item.children = children + + self.log.debug("Adding children for {0}".format(item.type_)) + + for child_type, child_rules in item.children: + child_item = self._new_browser_item(child_type, item, rules=child_rules) + item.addChild(child_item) + + item.child_populated = True + self.busy.reset() + # # Results runner # - def run(self, button): # right now there is only one button. fail = False @@ -234,10 +323,67 @@ class DomainTransitionAnalysisTab(SEToolsWidget, QScrollArea): self.busy.repaint() self.raw_results.moveCursor(QTextCursor.Start) + if self.flows_in.isChecked() or self.flows_out.isChecked(): + # move to browser tab for transitions in/out + self.results_frame.setCurrentIndex(1) + else: + self.results_frame.setCurrentIndex(0) + self.busy.reset() -class ResultsUpdater(QObject): +def print_transition(renderer, trans): + """ + Raw rendering of a domain transition. + + Parameters: + renderer A callable which will render the output, e.g. print + trans A step (transition object) generated by the DTA. + """ + + if trans.transition: + renderer("Domain transition rule(s):") + for t in trans.transition: + renderer(str(t)) + + if trans.setexec: + renderer("\nSet execution context rule(s):") + for s in trans.setexec: + renderer(str(s)) + + for entrypoint in trans.entrypoints: + renderer("\nEntrypoint {0}:".format(entrypoint.name)) + + renderer("\tDomain entrypoint rule(s):") + for e in entrypoint.entrypoint: + renderer("\t{0}".format(e)) + + renderer("\n\tFile execute rule(s):") + for e in entrypoint.execute: + renderer("\t{0}".format(e)) + + if entrypoint.type_transition: + renderer("\n\tType transition rule(s):") + for t in entrypoint.type_transition: + renderer("\t{0}".format(t)) + + renderer("") + + if trans.dyntransition: + renderer("Dynamic transition rule(s):") + for d in trans.dyntransition: + renderer(str(d)) + + renderer("\nSet current process context rule(s):") + for s in trans.setcurrent: + renderer(str(s)) + + renderer("") + + renderer("") + + +class ResultsUpdater(QThread): """ Thread for processing queries and updating result widgets. @@ -247,22 +393,28 @@ class ResultsUpdater(QObject): 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. + trans (str, bool, list) Initial information for populating + the transitions browser. """ - finished = pyqtSignal() raw_line = pyqtSignal(str) + trans = pyqtSignal(str, bool, list) def __init__(self, query): super(ResultsUpdater, self).__init__() self.query = query self.log = logging.getLogger(__name__) - def update(self): + def __del__(self): + self.wait() + + def run(self): """Run the query and update results.""" assert self.query.limit, "Code doesn't currently handle unlimited (limit=0) paths." + self.out = self.query.mode == "flows_out" + if self.query.mode == "all_paths": self.transitive(self.query.all_paths(self.query.source, self.query.target, self.query.max_path_len)) @@ -273,52 +425,6 @@ class ResultsUpdater(QObject): else: # flows_in self.direct(self.query.transitions(self.query.target)) - self.finished.emit() - - def print_transition(self, trans): - """Raw rendering of a domain transition.""" - - if trans.transition: - self.raw_line.emit("Domain transition rule(s):") - for t in trans.transition: - self.raw_line.emit(str(t)) - - if trans.setexec: - self.raw_line.emit("\nSet execution context rule(s):") - for s in trans.setexec: - self.raw_line.emit(str(s)) - - for entrypoint in trans.entrypoints: - self.raw_line.emit("\nEntrypoint {0}:".format(entrypoint.name)) - - self.raw_line.emit("\tDomain entrypoint rule(s):") - for e in entrypoint.entrypoint: - self.raw_line.emit("\t{0}".format(e)) - - self.raw_line.emit("\n\tFile execute rule(s):") - for e in entrypoint.execute: - self.raw_line.emit("\t{0}".format(e)) - - if entrypoint.type_transition: - self.raw_line.emit("\n\tType transition rule(s):") - for t in entrypoint.type_transition: - self.raw_line.emit("\t{0}".format(t)) - - self.raw_line.emit("") - - if trans.dyntransition: - self.raw_line.emit("Dynamic transition rule(s):") - for d in trans.dyntransition: - self.raw_line.emit(str(d)) - - self.raw_line.emit("\nSet current process context rule(s):") - for s in trans.setcurrent: - self.raw_line.emit(str(s)) - - self.raw_line.emit("") - - self.raw_line.emit("") - def transitive(self, paths): i = 0 for i, path in enumerate(paths, start=1): @@ -328,7 +434,7 @@ class ResultsUpdater(QObject): self.raw_line.emit("Step {0}: {1} -> {2}\n".format(stepnum, step.source, step.target)) - self.print_transition(step) + print_transition(self.raw_line.emit, step) if QThread.currentThread().isInterruptionRequested() or (i >= self.query.limit): break @@ -340,14 +446,72 @@ class ResultsUpdater(QObject): def direct(self, transitions): i = 0 + child_types = [] for i, step in enumerate(transitions, start=1): self.raw_line.emit("Transition {0}: {1} -> {2}\n".format(i, step.source, step.target)) - self.print_transition(step) + print_transition(self.raw_line.emit, step) - if QThread.currentThread().isInterruptionRequested() or (i >= self.query.limit): + # Generate results for flow browser + if self.out: + child_types.append((step.target, step)) + else: + child_types.append((step.source, step)) + + if QThread.currentThread().isInterruptionRequested(): break else: QThread.yieldCurrentThread() self.raw_line.emit("{0} domain transition(s) found.".format(i)) self.log.info("{0} domain transition(s) found.".format(i)) + + # Update browser: + root_type = self.query.source if self.out else self.query.target + self.trans.emit(str(root_type), self.out, sorted(child_types)) + + +class BrowserUpdater(QThread): + + """ + Thread for processing additional analysis for the browser. + + Parameters: + query The query object + model The model for the results + + Qt signals: + trans A list of child types to render in the + transitions browser. + """ + + trans = pyqtSignal(list) + + def __init__(self, query): + super(BrowserUpdater, self).__init__() + self.query = query + self.type_ = None + self.out = None + self.log = logging.getLogger(__name__) + + def __del__(self): + self.wait() + + def run(self): + transnum = 0 + child_types = [] + for transnum, trans in enumerate(self.query.transitions(self.type_), start=1): + # Generate results for browser + if self.out: + child_types.append((trans.target, trans)) + else: + child_types.append((trans.source, trans)) + + if QThread.currentThread().isInterruptionRequested(): + break + else: + QThread.yieldCurrentThread() + + self.log.debug("{0} additional domain transition(s) found.".format(transnum)) + + # Update browser: + self.trans.emit(sorted(child_types)) diff --git a/setoolsgui/apol/infoflow.py b/setoolsgui/apol/infoflow.py index 5f3b4d4..a021d3e 100644 --- a/setoolsgui/apol/infoflow.py +++ b/setoolsgui/apol/infoflow.py @@ -20,9 +20,10 @@ import logging import copy -from PyQt5.QtCore import pyqtSignal, Qt, QObject, QStringListModel, QThread +from PyQt5.QtCore import pyqtSignal, Qt, QStringListModel, QThread from PyQt5.QtGui import QPalette, QTextCursor -from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea +from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea, \ + QTreeWidgetItem from setools import InfoFlowAnalysis from setools.exception import UnmappedClass, UnmappedPermission @@ -100,13 +101,14 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): self.clear_target_error() # set up processing thread - self.thread = QThread() - self.worker = ResultsUpdater(self.query) - 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) + self.thread = ResultsUpdater(self.query) + self.thread.raw_line.connect(self.raw_results.appendPlainText) + self.thread.finished.connect(self.update_complete) + self.thread.flows.connect(self.reset_browser) + + # set up browser thread + self.browser_thread = BrowserUpdater(self.query) + self.browser_thread.flows.connect(self.add_browser_children) # create a "busy, please wait" dialog self.busy = QProgressDialog(self) @@ -127,6 +129,7 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): self.target.setEnabled(not self.flows_out.isChecked()) self.criteria_frame.setHidden(not self.criteria_expander.isChecked()) self.notes.setHidden(not self.notes_expander.isChecked()) + self.browser_tab.setEnabled(self.flows_in.isChecked() or self.flows_out.isChecked()) # connect signals self.buttonBox.clicked.connect(self.run) @@ -140,6 +143,7 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): self.min_perm_weight.valueChanged.connect(self.set_min_weight) self.exclude_types.clicked.connect(self.choose_excluded_types) self.edit_permmap.clicked.connect(self.open_permmap_editor) + self.browser.currentItemChanged.connect(self.browser_item_selected) # # Analysis mode @@ -153,11 +157,15 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): self.clear_source_error() self.clear_target_error() self.source.setEnabled(not value) + self.limit_paths.setEnabled(not value) + self.browser_tab.setEnabled(value) def flows_out_toggled(self, value): self.clear_source_error() self.clear_target_error() self.target.setEnabled(not value) + self.limit_paths.setEnabled(not value) + self.browser_tab.setEnabled(value) # # Source criteria @@ -222,10 +230,92 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): # used only by permission map editor self.query.perm_map = pmap + # + # Infoflow browser + # + def _new_browser_item(self, type_, parent, rules=None, children=None): + # build main item + item = QTreeWidgetItem(parent if parent else self.browser) + item.setText(0, str(type_)) + item.type_ = type_ + item.children = children if children else [] + item.rules = rules if rules else [] + item.child_populated = children is not None + + # add child items + for child_type, child_rules in item.children: + child_item = self._new_browser_item(child_type, item, rules=child_rules) + item.addChild(child_item) + + item.setExpanded(children is not None) + + self.log.debug("Built item for {0} with {1} children and {2} rules".format( + type_, len(item.children), len(item.rules))) + + return item + + def reset_browser(self, root_type, out, children): + self.log.debug("Resetting browser.") + + # clear results + self.browser.clear() + self.browser_details.clear() + + # save browser details independent + # from main analysis UI settings + self.browser_root_type = root_type + self.browser_mode = out + + root = self._new_browser_item(self.browser_root_type, self.browser, children=children) + + self.browser.insertTopLevelItem(0, root) + + def browser_item_selected(self, current, previous): + if not current: + # browser is being reset + return + + self.log.debug("{0} selected in browser.".format(current.type_)) + self.browser_details.clear() + + try: + parent_type = current.parent().type_ + except AttributeError: + # should only hit his on the root item + pass + else: + self.browser_details.appendPlainText("Information flows {0} {1} {2}\n".format( + current.parent().type_, "->" if self.browser_mode else "<-", current.type_)) + + for rule in current.rules: + self.browser_details.appendPlainText(rule) + + self.browser_details.moveCursor(QTextCursor.Start) + + if not current.child_populated: + self.busy.setLabelText("Gathering additional browser details for {0}...".format( + current.type_)) + self.busy.show() + self.browser_thread.out = self.browser_mode + self.browser_thread.type_ = current.type_ + self.browser_thread.start() + + def add_browser_children(self, children): + item = self.browser.currentItem() + item.children = children + + self.log.debug("Adding children for {0}".format(item.type_)) + + for child_type, child_rules in item.children: + child_item = self._new_browser_item(child_type, item, rules=child_rules) + item.addChild(child_item) + + item.child_populated = True + self.busy.reset() + # # Results runner # - def run(self, button): # right now there is only one button. fail = False @@ -267,10 +357,16 @@ class InfoFlowAnalysisTab(SEToolsWidget, QScrollArea): self.busy.repaint() self.raw_results.moveCursor(QTextCursor.Start) + if self.flows_in.isChecked() or self.flows_out.isChecked(): + # move to browser tab for flows in/out + self.results_frame.setCurrentIndex(1) + else: + self.results_frame.setCurrentIndex(0) + self.busy.reset() -class ResultsUpdater(QObject): +class ResultsUpdater(QThread): """ Thread for processing queries and updating result widgets. @@ -280,33 +376,37 @@ class ResultsUpdater(QObject): 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. + raw_line A string to be appended to the raw results. + flows (str, bool, list) Initial information for populating + the flows browser. """ - finished = pyqtSignal() raw_line = pyqtSignal(str) + flows = pyqtSignal(str, bool, list) def __init__(self, query): super(ResultsUpdater, self).__init__() self.query = query self.log = logging.getLogger(__name__) - def update(self): + def __del__(self): + self.wait() + + def run(self): """Run the query and update results.""" assert self.query.limit, "Code doesn't currently handle unlimited (limit=0) paths." + self.out = self.query.mode == "flows_out" + if self.query.mode == "all_paths": self.transitive(self.query.all_paths(self.query.source, self.query.target, self.query.max_path_len)) elif self.query.mode == "all_shortest_paths": self.transitive(self.query.all_shortest_paths(self.query.source, self.query.target)) elif self.query.mode == "flows_out": - self.direct(self.query.infoflows(self.query.source, out=True)) + self.direct(self.query.infoflows(self.query.source, out=self.out)) else: # flows_in - self.direct(self.query.infoflows(self.query.target, out=False)) - - self.finished.emit() + self.direct(self.query.infoflows(self.query.target, out=self.out)) def transitive(self, paths): pathnum = 0 @@ -334,6 +434,7 @@ class ResultsUpdater(QObject): def direct(self, flows): flownum = 0 + child_types = [] for flownum, flow in enumerate(flows, start=1): self.raw_line.emit("Flow {0}: {1} -> {2}".format(flownum, flow.source, flow.target)) for rule in sorted(flow.rules): @@ -341,10 +442,67 @@ class ResultsUpdater(QObject): self.raw_line.emit("") - if QThread.currentThread().isInterruptionRequested() or (flownum >= self.query.limit): + # Generate results for flow browser + if self.out: + child_types.append((flow.target, sorted(str(r) for r in flow.rules))) + else: + child_types.append((flow.source, sorted(str(r) for r in flow.rules))) + + if QThread.currentThread().isInterruptionRequested(): break else: QThread.yieldCurrentThread() self.raw_line.emit("{0} information flow(s) found.\n".format(flownum)) self.log.info("{0} information flow(s) found.".format(flownum)) + + # Update browser: + root_type = self.query.source if self.out else self.query.target + self.flows.emit(str(root_type), self.out, sorted(child_types)) + + +class BrowserUpdater(QThread): + + """ + Thread for processing additional analysis for the browser. + + Parameters: + query The query object + model The model for the results + + Qt signals: + flows A list of child types to render in the + infoflows browser. + """ + + flows = pyqtSignal(list) + + def __init__(self, query): + super(BrowserUpdater, self).__init__() + self.query = query + self.type_ = None + self.out = None + self.log = logging.getLogger(__name__) + + def __del__(self): + self.wait() + + def run(self): + flownum = 0 + child_types = [] + for flownum, flow in enumerate(self.query.infoflows(self.type_, out=self.out), start=1): + # Generate results for flow browser + if self.out: + child_types.append((flow.target, sorted(str(r) for r in flow.rules))) + else: + child_types.append((flow.source, sorted(str(r) for r in flow.rules))) + + if QThread.currentThread().isInterruptionRequested(): + break + else: + QThread.yieldCurrentThread() + + self.log.debug("{0} additional information flow(s) found.".format(flownum)) + + # Update browser: + self.flows.emit(sorted(child_types))