ui,popups: allow to update checksums

When the checksum of a binary changes, due to an update or something
else, you'll be prompted to allow the outbound connection if the
previous checksum of the rule doesn't match the new one.

Without a visual warning was almost impossible to know what was going
on. Besides, you had to dismiss that pop-up, find the rule, and update
the checksum.

Now there's a warning message, and you can update the rule from
the pop-up.

Related: #413
This commit is contained in:
Gustavo Iñiguez Goia 2023-11-24 00:57:13 +01:00
parent ffd70836f6
commit 02cf65ac33
Failed to generate hash of commit
4 changed files with 380 additions and 137 deletions

View file

@ -584,6 +584,25 @@ class Database:
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

View file

@ -19,6 +19,7 @@ from opensnitch.config import Config
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
# 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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
@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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 (<a href='#'>More info</a>)"
)
)
self.labelChecksumNote.setText(
QC.translate("popups", "<font color=\"red\">WARNING, checksums differ.</font><br><br>Current process ({0}):<br>{1}<br><br>Expected from the rule:<br>{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 "<b>WARNING</b><br>bad md5<br>This process:{0}<br>Expected from rule: {1}<br><br>".format(
con.process_checksums[Config.OPERAND_PROCESS_HASH_MD5],
expected
)
return ""
def _render_details(self, peer, detailsWidget, con):
tree = ""
space = "&nbsp;"
spaces = "&nbsp;"
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}<p>│{1}\t{2}{3}{4}</p>".format(tree, path.value, spaces, indicator, path.key)
spaces += "&nbsp;" * 4
indicator = "\\_ "
# XXX: table element doesn't work?
details = """<b>{0}</b> {1}:{2} -> {3}:{4}
<br><br>
<b>Path:</b>{5}{6}<br>
<b>Cmdline:</b>&nbsp;{7}<br>
<b>CWD:</b>{8}{9}<br>
<b>MD5:</b>{10}{11}<br>
<b>UID:</b>{12}{13}<br>
<b>PID:</b>{14}{15}<br>
<br>
<b>Process tree:</b><br>
{16}
<br>
<p><b>Environment variables:<b></p>
{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('<p>{}={}</p>'.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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
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 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.setFixedSize(self.size())
def _render_details(self, con):
tree = ""
space = "&nbsp;"
spaces = "&nbsp;"
indicator = ""
try:
# reverse() doesn't exist on old protobuf libs.
con.process_tree.reverse()
except:
pass
for path in con.process_tree:
tree = "{0}<p>│{1}\t{2}{3}{4}</p>".format(tree, path.value, spaces, indicator, path.key)
spaces += "&nbsp;" * 4
indicator = "\_ "
# XXX: table element doesn't work?
details = """<b>{0}</b> {1}:{2} -> {3}:{4}
<br><br>
<b>Path:</b>{5}{6}<br>
<b>Cmdline:</b>&nbsp;{7}<br>
<b>CWD:</b>{8}{9}<br>
<b>MD5:</b>{10}{11}<br>
<b>UID:</b>{12}{13}<br>
<b>PID:</b>{14}{15}<br>
<br>
<b>Process tree:</b><br>
{16}
<br>
<p><b>Environment variables:<b></p>
{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('<p>{}={}</p>'.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):

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>563</width>
<height>323</height>
<height>339</height>
</rect>
</property>
<property name="sizePolicy">
@ -188,7 +188,7 @@
</property>
<property name="icon">
<iconset theme="emblem-important">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
@ -223,7 +223,7 @@
</property>
<property name="icon">
<iconset theme="emblem-default">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
<normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../../../../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
@ -263,7 +263,7 @@
<item row="0" column="0">
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>1</number>
<number>2</number>
</property>
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout_5">
@ -297,7 +297,7 @@
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>.</normaloff>.</iconset>
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
@ -328,8 +328,105 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<layout class="QGridLayout" name="gridLayout_6">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="cmdBackChecksums">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-previous">
<normaloff>../../../../../../.designer/backup</normaloff>../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QPushButton" name="cmdUpdateRule">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Update rule</string>
</property>
<property name="icon">
<iconset theme="reload">
<normaloff>../../../../../../.designer/backup</normaloff>../../../../../../.designer/backup</iconset>
</property>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelChecksumNote">
<property name="text">
<string/>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QComboBox" name="comboChecksumRule"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="labelChecksumStatus">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0,0,0,1">
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0,0,0,0,0">
<property name="leftMargin">
<number>2</number>
</property>
@ -345,6 +442,44 @@
<property name="spacing">
<number>0</number>
</property>
<item row="3" column="0">
<widget class="QLabel" name="messageLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">Chromium Web Browser wants to connect to www.evilsocket.net on tcp port 443. And maybe to www.goodsocket.net on port 344</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>5</number>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="bottomMargin">
@ -412,6 +547,26 @@
<property name="spacing">
<number>2</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="cmdInfo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="dialog-information">
<normaloff>../../../../../../../../../../.designer/backup</normaloff>../../../../../../../../../../.designer/backup</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="appNameLabel">
<property name="sizePolicy">
@ -445,24 +600,35 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="cmdInfo">
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="argsLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string/>
<string notr="true">chromium -arg1 -arg2</string>
</property>
<property name="icon">
<iconset theme="dialog-information">
<normaloff>.</normaloff>.</iconset>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="flat">
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
@ -506,38 +672,7 @@
</sizepolicy>
</property>
<property name="text">
<string notr="true">(/path/to/bin/chromium)</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="argsLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string notr="true">(/path/to/bin/chromium)</string>
<string notr="true">(/absolute/path/to/bin/chromium)</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
@ -557,20 +692,7 @@
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<property name="topMargin">
<number>6</number>
@ -946,31 +1068,6 @@
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="messageLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">Chromium Web Browser wants to connect to www.evilsocket.net on tcp port 443. And maybe to www.goodsocket.net on port 344</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="margin">
<number>5</number>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View file

@ -132,6 +132,12 @@ class Rules(QObject):
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)