diff --git a/daemon/rule/rule.go b/daemon/rule/rule.go index b51cf8fa..0fb04bdf 100644 --- a/daemon/rule/rule.go +++ b/daemon/rule/rule.go @@ -33,26 +33,28 @@ const ( // The fields match the ones saved as json to disk. // If a .json rule file is modified on disk, it's reloaded automatically. type Rule struct { - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Name string `json:"name"` - Enabled bool `json:"enabled"` - Precedence bool `json:"precedence"` - Action Action `json:"action"` - Duration Duration `json:"duration"` - Operator Operator `json:"operator"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Name string `json:"name"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + Precedence bool `json:"precedence"` + Action Action `json:"action"` + Duration Duration `json:"duration"` + Operator Operator `json:"operator"` } // Create creates a new rule object with the specified parameters. -func Create(name string, enabled bool, precedence bool, action Action, duration Duration, op *Operator) *Rule { +func Create(name, description string, enabled bool, precedence bool, action Action, duration Duration, op *Operator) *Rule { return &Rule{ - Created: time.Now(), - Enabled: enabled, - Precedence: precedence, - Name: name, - Action: action, - Duration: duration, - Operator: *op, + Created: time.Now(), + Enabled: enabled, + Precedence: precedence, + Name: name, + Description: description, + Action: action, + Duration: duration, + Operator: *op, } } @@ -86,6 +88,7 @@ func Deserialize(reply *protocol.Rule) (*Rule, error) { return Create( reply.Name, + reply.Description, reply.Enabled, reply.Precedence, Action(reply.Action), @@ -100,11 +103,12 @@ func (r *Rule) Serialize() *protocol.Rule { return nil } return &protocol.Rule{ - Name: string(r.Name), - Enabled: bool(r.Enabled), - Precedence: bool(r.Precedence), - Action: string(r.Action), - Duration: string(r.Duration), + Name: string(r.Name), + Description: string(r.Description), + Enabled: bool(r.Enabled), + Precedence: bool(r.Precedence), + Action: string(r.Action), + Duration: string(r.Duration), Operator: &protocol.Operator{ Type: string(r.Operator.Type), Sensitive: bool(r.Operator.Sensitive), diff --git a/daemon/ui/client.go b/daemon/ui/client.go index 010a0bbb..6fc78480 100644 --- a/daemon/ui/client.go +++ b/daemon/ui/client.go @@ -23,10 +23,10 @@ import ( var ( configFile = "/etc/opensnitchd/default-config.json" dummyOperator, _ = rule.NewOperator(rule.Simple, false, rule.OpTrue, "", make([]rule.Operator, 0)) - clientDisconnectedRule = rule.Create("ui.client.disconnected", true, false, rule.Allow, rule.Once, dummyOperator) + clientDisconnectedRule = rule.Create("ui.client.disconnected", "", true, false, rule.Allow, rule.Once, dummyOperator) // While the GUI is connected, deny by default everything until the user takes an action. - clientConnectedRule = rule.Create("ui.client.connected", true, false, rule.Deny, rule.Once, dummyOperator) - clientErrorRule = rule.Create("ui.client.error", true, false, rule.Allow, rule.Once, dummyOperator) + clientConnectedRule = rule.Create("ui.client.connected", "", true, false, rule.Deny, rule.Once, dummyOperator) + clientErrorRule = rule.Create("ui.client.error", "", true, false, rule.Allow, rule.Once, dummyOperator) config Config ) diff --git a/proto/ui.proto b/proto/ui.proto index f4224d25..fc46ae06 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -71,11 +71,12 @@ message Operator { message Rule { string name = 1; - bool enabled = 2; - bool precedence = 3; - string action = 4; - string duration = 5; - Operator operator = 6; + string description = 2; + bool enabled = 3; + bool precedence = 4; + string action = 5; + string duration = 6; + Operator operator = 7; } enum Action { diff --git a/ui/opensnitch/database.py b/ui/opensnitch/database/__init__.py similarity index 87% rename from ui/opensnitch/database.py rename to ui/opensnitch/database/__init__.py index ed93a115..83b69ad7 100644 --- a/ui/opensnitch/database.py +++ b/ui/opensnitch/database/__init__.py @@ -1,6 +1,7 @@ from PyQt5.QtSql import QSqlDatabase, QSqlQueryModel, QSqlQuery import threading import sys +import os from datetime import datetime, timedelta class Database: @@ -10,6 +11,8 @@ class Database: DB_TYPE_MEMORY = 0 DB_TYPE_FILE = 1 + DB_VERSION = 1 + @staticmethod def instance(): if Database.__instance == None: @@ -26,6 +29,8 @@ class Database: if dbtype != Database.DB_TYPE_MEMORY: self.db_file = dbfile + is_new_file = not os.path.isfile(self.db_file) + self.db = QSqlDatabase.addDatabase("QSQLITE", self.db_name) self.db.setDatabaseName(self.db_file) if not self.db.open(): @@ -38,7 +43,13 @@ class Database: print("db.initialize() error:", db_error) return False, db_error + + if is_new_file: + print("is new file, or IN MEMORY, setting initial schema version") + self.set_schema_version(self.DB_VERSION) + self._create_tables() + self._upgrade_db_schema() return True, None def close(self): @@ -77,6 +88,7 @@ class Database: def _create_tables(self): # https://www.sqlite.org/wal.html if self.db_file == Database.DB_IN_MEMORY: + self.set_schema_version(self.DB_VERSION) q = QSqlQuery("PRAGMA journal_mode = OFF", self.db) q.exec_() q = QSqlQuery("PRAGMA synchronous = OFF", self.db) @@ -137,6 +149,7 @@ class Database: "operator_sensitive text, " \ "operator_operand text, " \ "operator_data text, " \ + "description text, " \ "UNIQUE(node, name)" ")", self.db) q.exec_() @@ -168,6 +181,49 @@ class Database: , self.db) q.exec_() + def get_schema_version(self): + q = QSqlQuery("PRAGMA user_version;", self.db) + q.exec_() + if q.next(): + print("schema version:", q.value(0)) + return int(q.value(0)) + + return 0 + + def set_schema_version(self, version): + print("setting schema version to:", version) + q = QSqlQuery("PRAGMA user_version = {0}".format(version), self.db) + q.exec_() + + def _upgrade_db_schema(self): + migrations_path = os.path.dirname(os.path.realpath(__file__)) + "/migrations" + schema_version = self.get_schema_version() + if schema_version == self.DB_VERSION: + print("db schema is up to date") + return + while schema_version < self.DB_VERSION: + schema_version += 1 + try: + print("applying schema upgrade:", schema_version) + self._apply_db_upgrade("{0}/upgrade_{1}.sql".format(migrations_path, schema_version)) + except Exception: + print("Not applyging upgrade_{0}.sql".format(schema_version)) + self.set_schema_version(schema_version) + + def _apply_db_upgrade(self, file): + print("applying upgrade from:", file) + q = QSqlQuery(self.db) + with open(file) as f: + for line in f.readlines(): + # skip comments + if line.startswith("--"): + continue + print("applying upgrade:", line, end="") + if q.exec(line) == False: + print("\tError:", q.lastError().text()) + else: + print("\tOK") + def optimize(self): """https://www.sqlite.org/pragma.html#pragma_optimize """ @@ -430,9 +486,9 @@ class Database: def insert_rule(self, rule, node_addr): self.insert("rules", - "(time, node, name, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", + "(time, node, name, description, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - node_addr, rule.name, + node_addr, rule.name, rule.description, str(rule.enabled), str(rule.precedence), rule.action, rule.duration, rule.operator.type, str(rule.operator.sensitive), rule.operator.operand, rule.operator.data), diff --git a/ui/opensnitch/database/migrations/upgrade_1.sql b/ui/opensnitch/database/migrations/upgrade_1.sql new file mode 100644 index 00000000..c5f1be8a --- /dev/null +++ b/ui/opensnitch/database/migrations/upgrade_1.sql @@ -0,0 +1,2 @@ +--ALTER TABLE rules ADD COLUMN description; +create table x(id int); diff --git a/ui/opensnitch/dialogs/ruleseditor.py b/ui/opensnitch/dialogs/ruleseditor.py index 562c6ea6..b46319dd 100644 --- a/ui/opensnitch/dialogs/ruleseditor.py +++ b/ui/opensnitch/dialogs/ruleseditor.py @@ -269,6 +269,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): rule.operator.sensitive = self._bool(records.value(8)) rule.operator.operand = records.value(9) rule.operator.data = "" if records.value(10) == None else str(records.value(10)) + rule.description = records.value(11) return rule @@ -277,6 +278,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): self.rule = None self.ruleNameEdit.setText("") + self.ruleDescEdit.setPlainText("") self.statusLabel.setText("") self.actionDenyRadio.setChecked(True) @@ -327,6 +329,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): return False self.ruleNameEdit.setText(rule.name) + self.ruleDescEdit.setPlainText(rule.description) self.enableCheck.setChecked(rule.enabled) self.precedenceCheck.setChecked(rule.precedence) if rule.action == Config.ACTION_DENY: @@ -449,9 +452,9 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): def _insert_rule_to_db(self, node_addr): self._db.insert("rules", - "(time, node, name, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", + "(time, node, name, description, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - node_addr, self.rule.name, + node_addr, self.rule.name, self.rule.description, str(self.rule.enabled), str(self.rule.precedence), self.rule.action, self.rule.duration, self.rule.operator.type, str(self.rule.operator.sensitive), self.rule.operator.operand, self.rule.operator.data), @@ -509,6 +512,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): """ self.rule = ui_pb2.Rule() self.rule.name = self.ruleNameEdit.text() + self.rule.description = self.ruleDescEdit.toPlainText() self.rule.enabled = self.enableCheck.isChecked() self.rule.precedence = self.precedenceCheck.isChecked() self.rule.operator.type = Config.RULE_TYPE_SIMPLE diff --git a/ui/opensnitch/dialogs/stats.py b/ui/opensnitch/dialogs/stats.py index ae6d35c1..dae71eb3 100644 --- a/ui/opensnitch/dialogs/stats.py +++ b/ui/opensnitch/dialogs/stats.py @@ -200,7 +200,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): "filterLine": None, "model": None, "delegate": commonDelegateConf, - "display_fields": "*", + "display_fields": "time as Time," \ + "node as Node," \ + "name as Name," \ + "enabled as Enabled," \ + "action as Action," \ + "duration as Duration," \ + "description as Description", "header_labels": [], "last_order_by": "2", "last_order_to": 0, @@ -336,6 +342,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): self.COL_STR_TIME = QC.translate("stats", "Time", "This is a word, without spaces and symbols.") self.COL_STR_ACTION = QC.translate("stats", "Action", "This is a word, without spaces and symbols.") self.COL_STR_DURATION = QC.translate("stats", "Duration", "This is a word, without spaces and symbols.") + self.COL_STR_DESCRIPTION = QC.translate("stats", "Description", "This is a word, without spaces and symbols.") self.COL_STR_NODE = QC.translate("stats", "Node", "This is a word, without spaces and symbols.") self.COL_STR_ENABLED = QC.translate("stats", "Enabled", "This is a word, without spaces and symbols.") self.COL_STR_PRECEDENCE = QC.translate("stats", "Precedence", "This is a word, without spaces and symbols.") @@ -447,13 +454,9 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): self.COL_STR_NODE, self.COL_STR_NAME, self.COL_STR_ENABLED, - self.COL_STR_PRECEDENCE, self.COL_STR_ACTION, self.COL_STR_DURATION, - "operator_type", - "operator_sensitive", - "operator_operand", - "operator_data", + self.COL_STR_DESCRIPTION, ] stats_headers = [ @@ -492,8 +495,8 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): verticalScrollBar=self.verticalScrollBar, sort_direction=self.SORT_ORDER[1], delegate=self.TABLES[self.TAB_NODES]['delegate']) - self.TABLES[self.TAB_RULES]['view'] = self._setup_table(QtWidgets.QTableView, - self.rulesTable, "rules", + self.TABLES[self.TAB_RULES]['view'] = self._setup_table(QtWidgets.QTableView, self.rulesTable, "rules", + fields=self.TABLES[self.TAB_RULES]['display_fields'], model=GenericTableModel("rules", self.TABLES[self.TAB_RULES]['header_labels']), verticalScrollBar=self.rulesScrollBar, delegate=self.TABLES[self.TAB_RULES]['delegate'], @@ -688,6 +691,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): def _configure_buttons_icons(self): self.iconStart = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPlay")) self.iconPause = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPause")) + fwIcon = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_VistaShield")) self.newRuleButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_FileIcon"))) self.delRuleButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_TrashIcon"))) @@ -695,6 +699,7 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): self.saveButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogSaveButton"))) self.prefsButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_FileDialogDetailedView"))) self.startButton.setIcon(self.iconStart) + self.fwButton.setIcon(fwIcon) self.cmdProcDetails.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_FileDialogContentsView"))) self.TABLES[self.TAB_MAIN]['cmdCleanStats'].setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogResetButton"))) for idx in range(1,8): @@ -1869,7 +1874,12 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): what = what + " AND" what = what + " r.name LIKE '%{0}%'".format(filter_text) model = self._get_active_table().model() - self.setQuery(model, "SELECT * FROM rules as r %s %s %s" % (what, self._get_order(), self._get_limit())) + self.setQuery(model, "SELECT {0} FROM rules as r {1} {2} {3}".format( + self.TABLES[self.TAB_RULES]['display_fields'], + what, + self._get_order(), + self._get_limit() + )) self._restore_details_view_columns( self.TABLES[self.TAB_RULES]['view'].horizontalHeader(), "{0}{1}".format(Config.STATS_VIEW_COL_STATE, self.TAB_RULES) diff --git a/ui/opensnitch/nodes.py b/ui/opensnitch/nodes.py index 29d0ee28..e663c4ec 100644 --- a/ui/opensnitch/nodes.py +++ b/ui/opensnitch/nodes.py @@ -70,27 +70,27 @@ class Nodes(QObject): def add_fw_rules(self, addr, fwconfig): self._nodes[addr]['fwrules'] = fwconfig - def add_rule(self, time, node, name, enabled, precedence, action, duration, op_type, op_sensitive, op_operand, op_data): + def add_rule(self, time, node, name, description, enabled, precedence, action, duration, op_type, op_sensitive, op_operand, op_data): # don't add rule if the user has selected to exclude temporary # rules if duration in Config.RULES_DURATION_FILTER: return self._db.insert("rules", - "(time, node, name, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", - (time, node, name, enabled, precedence, action, duration, op_type, op_sensitive, op_operand, op_data), + "(time, node, name, description, enabled, precedence, action, duration, operator_type, operator_sensitive, operator_operand, operator_data)", + (time, node, name, description, enabled, precedence, action, duration, op_type, op_sensitive, op_operand, op_data), action_on_conflict="REPLACE") def add_rules(self, addr, rules): try: for _,r in enumerate(rules): self.add_rule(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - addr, - r.name, str(r.enabled), str(r.precedence), r.action, r.duration, - r.operator.type, - str(r.operator.sensitive), - r.operator.operand, - r.operator.data) + addr, + r.name, r.description, str(r.enabled), + str(r.precedence), r.action, r.duration, + r.operator.type, + str(r.operator.sensitive), + r.operator.operand, r.operator.data) except Exception as e: print(self.LOG_TAG + " exception adding node to db: ", e) diff --git a/ui/opensnitch/res/ruleseditor.ui b/ui/opensnitch/res/ruleseditor.ui index 91d25bbe..03309a64 100644 --- a/ui/opensnitch/res/ruleseditor.ui +++ b/ui/opensnitch/res/ruleseditor.ui @@ -6,39 +6,202 @@ 0 0 - 540 - 490 + 552 + 528 + + + 0 + 0 + + Rule - - - - - - Enable - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + + 0 + 0 + + + + false + + + false + + + false + + + + QLayout::SetDefaultConstraint + + + 0 + + + 2 + + + 12 + + + + + Action + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Duration + + + + + + + + once + + + + + 30s + + + + + 5m + + + + + 15m + + + + + 30m + + + + + 1h + + + + + until reboot + + + + + always + + + + + + + + + + + 0 + 0 + + + + Deny will just discard the connection + + + Deny + + + + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + + + true + + + + + + + Reject will drop the connection, and kill the socket that initiated it + + + Reject + + + + .. + + + + + + + + 0 + 0 + + + + Allow will allow the connection + + + Qt::LeftToRight + + + Allow + + + + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + + + + + + + - + @@ -80,7 +243,63 @@ You must name the rule in such manner that it'll be checked first, because they' - + + + + + 0 + 0 + + + + + 16777215 + 60 + + + + Description... + + + + + + + + + Enable + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + Name + + + + @@ -110,7 +329,53 @@ You must name the rule in such manner that it'll be checked first, because they' - + + + + true + + + + + + true + + + + + + + true + + + + 0 + 0 + + + + The rules are checked in alphabetical order, so you can name them accordingly to prioritize them. + +000-allow-localhost +001-deny-broadcast +... + + + leave blank to autocreate + + + true + + + + + + + Qt::Horizontal + + + + @@ -144,7 +409,7 @@ You must name the rule in such manner that it'll be checked first, because they' - + 0 @@ -155,9 +420,10 @@ You must name the rule in such manner that it'll be checked first, because they' true - + - + + .. Applications @@ -213,7 +479,7 @@ You must name the rule in such manner that it'll be checked first, because they' false - <html><head/><body><p>This field will only match the executable path. It is not modifiable by the user.<br/></p><p>You can use regular expressions to deny executions from /tmp for example:<br/></p><p>^/tmp/.*$</p></body></html> + <html><head/><body><p>The value of this field is always the absolute path to the executable: /path/to/binary<br/></p><p>Examples:</p><p>- Simple: /path/to/binary</p><p>- Multiple paths: ^/usr/lib(64|)/firefox/firefox$</p><p>- Multiple binaries: ^(/usr/sbin/ntpd|/lib/systemd/systemd-timesyncd|/usr/bin/xbrlapi|/usr/bin/dirmngr)$ </p><p>- Deny/Allow executions from /tmp:</p><p>^/(var/|)tmp/.*$<br/></p><p>For more examples visit the <a href="https://github.com/evilsocket/opensnitch/wiki/Rules-examples">wiki page</a> or ask on the <a href="https://github.com/evilsocket/opensnitch/discussions">Discussion forums</a>.</p></body></html> /path/to/executable, .*/bin/executable[0-9\.]+$, ... @@ -279,9 +545,10 @@ You must name the rule in such manner that it'll be checked first, because they' - + - + + .. Network @@ -528,9 +795,10 @@ Note: Commas or spaces are not allowed to separate IPs or networks. - + - + + .. List of domains/IPs @@ -561,7 +829,8 @@ Note: Commas or spaces are not allowed to separate IPs or networks. - + + .. @@ -595,7 +864,8 @@ Note: Commas or spaces are not allowed to separate IPs or networks. - + + .. @@ -664,7 +934,8 @@ Note: Commas or spaces are not allowed to separate IPs or networks. - + + .. @@ -684,222 +955,6 @@ Note: Commas or spaces are not allowed to separate IPs or networks. - - - - true - - - - - - true - - - - - - - true - - - The rules are checked in alphabetical order, so you can name them accordingly to prioritize them. - -000-allow-localhost -001-deny-broadcast -... - - - leave blank to autocreate - - - - - - - Qt::Horizontal - - - - - - - false - - - false - - - false - - - - QLayout::SetDefaultConstraint - - - 0 - - - 2 - - - 12 - - - - - Action - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Duration - - - - - - - - once - - - - - 30s - - - - - 5m - - - - - 15m - - - - - 30m - - - - - 1h - - - - - until reboot - - - - - always - - - - - - - - - - - 0 - 0 - - - - Deny - - - - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup - - - true - - - - - - - Reject - - - - .. - - - - - - - - 0 - 0 - - - - Qt::LeftToRight - - - Allow - - - - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup - - - - - - - - - - - - - 0 - 0 - - - - Name - - - diff --git a/ui/opensnitch/service.py b/ui/opensnitch/service.py index 9b066c3d..04ffded8 100644 --- a/ui/opensnitch/service.py +++ b/ui/opensnitch/service.py @@ -549,9 +549,10 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject): proto, addr = self._get_peer(kwargs['peer']) self._nodes.add_rule((datetime.now().strftime("%Y-%m-%d %H:%M:%S")), "{0}:{1}".format(proto, addr), - rule.name, str(rule.enabled), str(rule.precedence), rule.action, rule.duration, - rule.operator.type, str(rule.operator.sensitive), rule.operator.operand, - rule.operator.data) + rule.name, rule.description, str(rule.enabled), + str(rule.precedence), rule.action, rule.duration, + rule.operator.type, str(rule.operator.sensitive), rule.operator.operand, + rule.operator.data) elif kwargs['action'] == self.DELETE_RULE: self._db.delete_rule(kwargs['name'], kwargs['addr'])