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} +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} -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 @@