diff --git a/ui/opensnitch/database/__init__.py b/ui/opensnitch/database/__init__.py index a2a35076b0..26681cfc6c 100644 --- a/ui/opensnitch/database/__init__.py +++ b/ui/opensnitch/database/__init__.py @@ -584,6 +584,25 @@ def get_rule(self, rule_name, node_addr=None): return q + def get_rule_by_field(self, node_addr=None, field=None, value=None): + """ + get rule records by field (process.path, etc) + """ + qstr = "SELECT * FROM rules WHERE {0} LIKE ?".format(field) + q = QSqlQuery(qstr, self.db) + if node_addr != None: + qstr = qstr + " AND node=?".format(node_addr) + + q.prepare(qstr) + q.addBindValue("%" + value + "%") + if node_addr != None: + q.addBindValue(node_addr) + if not q.exec_(): + print("get_rule_by_field() error:", q.lastError().driverText()) + return None + + return q + def get_rules(self, node_addr): """ get rule records, given the name of the rule and the node diff --git a/ui/opensnitch/dialogs/prompt.py b/ui/opensnitch/dialogs/prompt.py index 20c288d93b..cd91a8e07b 100644 --- a/ui/opensnitch/dialogs/prompt.py +++ b/ui/opensnitch/dialogs/prompt.py @@ -19,6 +19,7 @@ from opensnitch.version import version from opensnitch.actions import Actions from opensnitch.rules import Rules, Rule +from opensnitch.nodes import Nodes from opensnitch import ui_pb2 @@ -28,6 +29,10 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _tick_trigger = QtCore.pyqtSignal() _timeout_trigger = QtCore.pyqtSignal() + PAGE_MAIN = 2 + PAGE_DETAILS = 0 + PAGE_CHECKSUMS = 1 + DEFAULT_TIMEOUT = 15 # don't translate @@ -59,6 +64,7 @@ def __init__(self, parent=None, appicon=None): # Other interesting flags: QtCore.Qt.Tool | QtCore.Qt.BypassWindowManagerHint self._cfg = Config.get() self._rules = Rules.instance() + self._nodes = Nodes.instance() self.setupUi(self) self.setWindowIcon(appicon) @@ -108,6 +114,10 @@ def __init__(self, parent=None, appicon=None): self.cmdInfo.clicked.connect(self._cb_cmdinfo_clicked) self.cmdBack.clicked.connect(self._cb_cmdback_clicked) + self.cmdUpdateRule.clicked.connect(self._cb_update_rule_clicked) + self.cmdBackChecksums.clicked.connect(self._cb_cmdback_clicked) + self.messageLabel.linkActivated.connect(self._cb_warninglbl_clicked) + self.allowIcon = Icons.new(self, "emblem-default") denyIcon = Icons.new(self, "emblem-important") rejectIcon = Icons.new(self, "window-close") @@ -116,6 +126,7 @@ def __init__(self, parent=None, appicon=None): self.cmdInfo.setIcon(infoIcon) self.cmdBack.setIcon(backIcon) + self.cmdBackChecksums.setIcon(backIcon) self._default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY) @@ -197,7 +208,7 @@ def _check_advanced_toggled(self, state): self.checkSum.setVisible(self._con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] != "" and state) self.checksumLabel_2.setVisible(self._con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] != "" and state) self.checksumLabel.setVisible(self._con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] != "" and state) - self.stackedWidget.setCurrentIndex(1) + self.stackedWidget.setCurrentIndex(self.PAGE_MAIN) self._ischeckAdvanceded = state self.adjust_size() @@ -206,12 +217,57 @@ def _check_advanced_toggled(self, state): def _button_clicked(self): self._stop_countdown() + def _cb_warninglbl_clicked(self): + self._stop_countdown() + self.stackedWidget.setCurrentIndex(self.PAGE_CHECKSUMS) + def _cb_cmdinfo_clicked(self): - self.stackedWidget.setCurrentIndex(0) + self.stackedWidget.setCurrentIndex(self.PAGE_DETAILS) self._stop_countdown() + def _cb_update_rule_clicked(self): + self.labelChecksumStatus.setStyleSheet('') + curRule = self.comboChecksumRule.currentText() + if curRule == "": + return + + # get rule from the db + records = self._rules.get_by_name(self._peer, curRule) + if records == None or records.first() == False: + self.labelChecksumStatus.setStyleSheet('color: red') + self.labelChecksumStatus.setText("✘ " + QC.translate("popups", "Rule not updated, not found by name")) + return + # transform it to proto rule + rule_obj = Rule.new_from_records(records) + if rule_obj.operator.type != Config.RULE_TYPE_LIST: + if rule_obj.operator.operand == Config.OPERAND_PROCESS_HASH_MD5: + rule_obj.operator.data = self._con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] + else: + for op in rule_obj.operator.list: + if op.operand == Config.OPERAND_PROCESS_HASH_MD5: + op.data = self._con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] + break + # add it back again to the db + added = self._rules.add_rules(self._peer, [rule_obj]) + if not added: + self.labelChecksumStatus.setStyleSheet('color: red') + self.labelChecksumStatus.setText("✘ " + QC.translate("popups", "Rule not updated.")) + return + + self._nodes.send_notification( + self._peer, + ui_pb2.Notification( + id=int(str(time.time()).replace(".", "")), + type=ui_pb2.CHANGE_RULE, + data="", + rules=[rule_obj] + ) + ) + self.labelChecksumStatus.setStyleSheet('color: green') + self.labelChecksumStatus.setText("✔" + QC.translate("popups", "Rule updated.")) + def _cb_cmdback_clicked(self): - self.stackedWidget.setCurrentIndex(1) + self.stackedWidget.setCurrentIndex(self.PAGE_MAIN) self._stop_countdown() def _set_elide_text(self, widget, text, max_size=132): @@ -235,6 +291,14 @@ def promptUser(self, connection, is_local, peer): self._local = is_local self._peer = peer self._con = connection + + # XXX: workaround for protobufs that don't report the address of + # the node. In this case the addr is "unix:/local" + proto, addr = self._nodes.get_addr(peer) + self._peer = proto + if addr != None: + self._peer = proto+":"+addr + self._done.clear() # trigger and show dialog self._prompt_trigger.emit() @@ -267,12 +331,12 @@ def _timeout_worker(self): @QtCore.pyqtSlot() def on_connection_prompt_triggered(self): - self.stackedWidget.setCurrentIndex(1) + self.stackedWidget.setCurrentIndex(self.PAGE_MAIN) self._render_connection(self._con) if self._tick > 0: self.show() # render details after displaying the pop-up. - self._render_details(self._con) + self._render_details(self._peer, self.connDetails, self._con) @QtCore.pyqtSlot() def on_tick_triggered(self): @@ -344,6 +408,109 @@ def _set_app_args(self, app_name, app_args): self.argsLabel.setVisible(False) self.argsLabel.setText("") + def _verify_checksums(self, con, rule): + """return true if the checksum of a rule matches the one of the process + opening a connection. + """ + if rule.operator.type != Config.RULE_TYPE_LIST: + return True, "" + + if con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5] == "": + return True, "" + + for ro in rule.operator.list: + if ro.type == Config.RULE_TYPE_SIMPLE and ro.operand == Config.OPERAND_PROCESS_HASH_MD5: + if ro.data != con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5]: + return False, ro.data + + return True, "" + + def _display_checksums_warning(self, peer, con): + self.messageLabel.setStyleSheet('') + self.labelChecksumStatus.setText('') + + records = self._rules.get_by_field(peer, "operator_data", con.process_path) + + if records != None and records.first(): + rule = Rule.new_from_records(records) + validates, expected = self._verify_checksums(con, rule) + if not validates: + self.messageLabel.setStyleSheet('color: red') + self.messageLabel.setText( + QC.translate("popups", "WARNING, bad checksum (More info)" + ) + ) + self.labelChecksumNote.setText( + QC.translate("popups", "WARNING, checksums differ.

Current process ({0}):
{1}

Expected from the rule:
{2}" + .format( + con.process_id, + con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5], + expected + ))) + + self.comboChecksumRule.clear() + self.comboChecksumRule.addItem(rule.name) + while records.next(): + rule = Rule.new_from_records(records) + self.comboChecksumRule.addItem(rule.name) + + return "WARNING
bad md5
This process:{0}
Expected from rule: {1}

".format( + con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5], + expected + ) + + return "" + + def _render_details(self, peer, detailsWidget, con): + tree = "" + space = " " + spaces = " " + indicator = "" + + self._display_checksums_warning(peer, con) + + try: + # reverse() doesn't exist on old protobuf libs. + con.process_tree.reverse() + except: + pass + for path in con.process_tree: + tree = "{0}

│{1}\t{2}{3}{4}

".format(tree, path.value, spaces, indicator, path.key) + spaces += " " * 4 + indicator = "\\_ " + + # XXX: table element doesn't work? + details = """{0} {1}:{2} -> {3}:{4} +

+Path:{5}{6}
+Cmdline: {7}
+CWD:{8}{9}
+MD5:{10}{11}
+UID:{12}{13}
+PID:{14}{15}
+
+Process tree:
+{16} +
+

Environment variables:

+{17} +""".format( + con.protocol.upper(), + con.src_port, con.src_ip, con.dst_ip, con.dst_port, + space * 6, con.process_path, + " ".join(con.process_args), + space * 6, con.process_cwd, + space * 7, con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5], + space * 9, con.user_id, + space * 9, con.process_id, + tree, + "".join('

{}={}

'.format(key, value) for key, value in con.process_env.items()) +) + + detailsWidget.document().clear() + detailsWidget.document().setHtml(details) + detailsWidget.moveCursor(QtGui.QTextCursor.Start) + def _render_connection(self, con): app_name, app_icon, description, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal") app_args = " ".join(con.process_args) @@ -357,6 +524,7 @@ def _render_connection(self, con): if app_name == "": self.appPathLabel.setVisible(False) self.argsLabel.setVisible(False) + self.argsLabel.setText("") app_name = QC.translate("popups", "Unknown process %s" % con.process_path) self.appNameLabel.setText(QC.translate("popups", "Outgoing connection")) else: @@ -454,53 +622,6 @@ def _render_connection(self, con): self.setFixedSize(self.size()) - def _render_details(self, con): - tree = "" - space = " " - spaces = " " - indicator = "" - - try: - # reverse() doesn't exist on old protobuf libs. - con.process_tree.reverse() - except: - pass - for path in con.process_tree: - tree = "{0}

│{1}\t{2}{3}{4}

".format(tree, path.value, spaces, indicator, path.key) - spaces += " " * 4 - indicator = "\_ " - - # XXX: table element doesn't work? - details = """{0} {1}:{2} -> {3}:{4} -

-Path:{5}{6}
-Cmdline: {7}
-CWD:{8}{9}
-MD5:{10}{11}
-UID:{12}{13}
-PID:{14}{15}
-
-Process tree:
-{16} -
-

Environment variables:

-{17} -""".format( - con.protocol.upper(), - con.src_port, con.src_ip, con.dst_ip, con.dst_port, - space * 6, con.process_path, - " ".join(con.process_args), - space * 6, con.process_cwd, - space * 7, con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5], - space * 9, con.user_id, - space * 9, con.process_id, - tree, - "".join('

{}={}

'.format(key, value) for key, value in con.process_env.items()) -) - - self.connDetails.document().clear() - self.connDetails.document().setHtml(details) - self.connDetails.moveCursor(QtGui.QTextCursor.Start) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): diff --git a/ui/opensnitch/res/prompt.ui b/ui/opensnitch/res/prompt.ui index 25f08d9c6a..ee97c7ac1a 100644 --- a/ui/opensnitch/res/prompt.ui +++ b/ui/opensnitch/res/prompt.ui @@ -7,7 +7,7 @@ 0 0 563 - 323 + 339 @@ -188,7 +188,7 @@ - ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../.designer/backup QToolButton::MenuButtonPopup @@ -223,7 +223,7 @@ - ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../.designer/backup @@ -263,7 +263,7 @@ - 1 + 2 @@ -297,7 +297,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -328,8 +328,105 @@ + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + ../../../../../../.designer/backup../../../../../../.designer/backup + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + Update rule + + + + ../../../../../../.designer/backup../../../../../../.designer/backup + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + - + 2 @@ -345,6 +442,44 @@ 0 + + + + + 0 + 0 + + + + Chromium Web Browser wants to connect to www.evilsocket.net on tcp port 443. And maybe to www.goodsocket.net on port 344 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + 5 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + @@ -412,6 +547,26 @@ 2 + + + + + 0 + 0 + + + + + + + + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + + + true + + + @@ -445,24 +600,35 @@ - - + + - + 0 0 + + + 10 + true + + - + chromium -arg1 -arg2 - - - .. + + Qt::PlainText - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + true + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + @@ -506,38 +672,7 @@ - (/path/to/bin/chromium) - - - Qt::PlainText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - - 10 - true - - - - (/path/to/bin/chromium) + (/absolute/path/to/bin/chromium) Qt::PlainText @@ -557,20 +692,7 @@ - - - - - 0 - 0 - - - - Qt::Horizontal - - - - + 6 @@ -946,31 +1068,6 @@ - - - - - 0 - 0 - - - - Chromium Web Browser wants to connect to www.evilsocket.net on tcp port 443. And maybe to www.goodsocket.net on port 344 - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - 5 - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - diff --git a/ui/opensnitch/rules.py b/ui/opensnitch/rules.py index 0834e0ab38..66f47ea614 100644 --- a/ui/opensnitch/rules.py +++ b/ui/opensnitch/rules.py @@ -132,6 +132,12 @@ def delete(self, name, addr, callback): def delete_by_field(self, field, values): return self._db.delete_rules_by_field(field, values) + def get_by_name(self, node, name): + return self._db.get_rule(name, node) + + def get_by_field(self, node, field, value): + return self._db.get_rule_by_field(node, field, value) + def exists(self, rule, node_addr): return self._db.rule_exists(rule, node_addr)