From fe66f9aa174dee33726e49fd3586b746b70c144c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20I=C3=B1iguez=20Goia?= Date: Fri, 21 Jun 2024 11:38:46 +0200 Subject: [PATCH] rules: improved operator list parsing and conversion Previously when creating a new rule we followed these steps: - Create a new protobuf Rule object from the ruleseditor or the pop-ups. - If the rule contained more than one operator, we converted the list of operators to a JSON string. - This JSON string was sent back to the daemon, and saved to the DB. - The list of operators were never expanded on the GUI, i.e., they were not saved as a list of protobuf Operator objects. - Once received in the daemon, the JSON string was parsed and converted to a protobuf Operator list of objects. Both, the JSON string and the list of protobuf Operator objects were saved to disk, but the JSON string was ignored when loading the rules. Saving the list of operators as a JSON string was a problem if you wanted to create or modify rules without the GUI. Now when creating or modifying rules from the GUI, the list of operators is no longer converted to JSON string. Instead the list is sent to the daemon as a list of protobuf Operators, and saved as JSON objects. Notes: - The JSON string is no longer saved to disk as part of the rules. - The list of operators is still saved as JSON string to the DB. - About not enabled rules: Previously, not enabled rules only had the list of operators as JSON string, with the field list:[] empty. Now the list of operators is saved as JSON objects, but if the rule is not enabled, it won't be parsed/loaded. Closes #1047 (cherry picked from commit b93051026e6a82ba07a5ac2f072880e69f04c238) --- daemon/rule/loader.go | 221 ++++++++++-------- daemon/rule/loader_test.go | 52 +++++ daemon/rule/rule.go | 43 +++- .../rule-disabled-operator-list-expanded.json | 31 +++ .../testdata/rule-disabled-operator-list.json | 17 ++ .../rule-operator-list-data-empty.json | 32 +++ daemon/rule/testdata/rule-operator-list.json | 31 +++ proto/ui.proto | 1 + ui/opensnitch/dialogs/prompt.py | 19 +- ui/opensnitch/dialogs/ruleseditor.py | 29 +-- ui/opensnitch/rules.py | 24 ++ ui/opensnitch/service.py | 4 + 12 files changed, 382 insertions(+), 122 deletions(-) create mode 100644 daemon/rule/testdata/rule-disabled-operator-list-expanded.json create mode 100644 daemon/rule/testdata/rule-disabled-operator-list.json create mode 100644 daemon/rule/testdata/rule-operator-list-data-empty.json create mode 100644 daemon/rule/testdata/rule-operator-list.json diff --git a/daemon/rule/loader.go b/daemon/rule/loader.go index 4a1c46db..b23e7988 100644 --- a/daemon/rule/loader.go +++ b/daemon/rule/loader.go @@ -98,6 +98,70 @@ func (l *Loader) Load(path string) error { return nil } +// Add adds a rule to the list of rules, and optionally saves it to disk. +func (l *Loader) Add(rule *Rule, saveToDisk bool) error { + l.addUserRule(rule) + if saveToDisk { + fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) + return l.Save(rule, fileName) + } + return nil +} + +// Replace adds a rule to the list of rules, and optionally saves it to disk. +func (l *Loader) Replace(rule *Rule, saveToDisk bool) error { + if err := l.replaceUserRule(rule); err != nil { + return err + } + if saveToDisk { + l.Lock() + defer l.Unlock() + + fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) + return l.Save(rule, fileName) + } + return nil +} + +// Save a rule to disk. +func (l *Loader) Save(rule *Rule, path string) error { + rule.Updated = time.Now().Format(time.RFC3339) + raw, err := json.MarshalIndent(rule, "", " ") + if err != nil { + return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) + } + + if err = ioutil.WriteFile(path, raw, 0600); err != nil { + return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) + } + + return nil +} + +// Delete deletes a rule from the list by name. +// If the duration is Always (i.e: saved on disk), it'll attempt to delete +// it from disk. +func (l *Loader) Delete(ruleName string) error { + l.Lock() + defer l.Unlock() + + rule := l.rules[ruleName] + if rule == nil { + return nil + } + l.cleanListsRule(rule) + + delete(l.rules, ruleName) + l.sortRules() + + if rule.Duration != Always { + return nil + } + + log.Info("Delete() rule: %s", rule) + return l.deleteRuleFromDisk(ruleName) +} + func (l *Loader) loadRule(fileName string) error { raw, err := ioutil.ReadFile(fileName) if err != nil { @@ -117,7 +181,13 @@ func (l *Loader) loadRule(fileName string) error { l.cleanListsRule(oldRule) } - if r.Enabled { + if !r.Enabled { + // XXX: we only parse and load the Data field if the rule is disabled and the Data field is not empty + // the rule will remain disabled. + if err = l.unmarshalOperatorList(&r.Operator); err != nil { + return err + } + } else { if err := r.Operator.Compile(); err != nil { log.Warning("Operator.Compile() error: %s: %s", err, r.Operator.Data) return fmt.Errorf("(1) Error compiling rule: %s", err) @@ -191,41 +261,6 @@ func (l *Loader) cleanListsRule(oldRule *Rule) { } } -func (l *Loader) liveReloadWorker() { - l.liveReloadRunning = true - - log.Debug("Rules watcher started on path %s ...", l.path) - if err := l.watcher.Add(l.path); err != nil { - log.Error("Could not watch path: %s", err) - l.liveReloadRunning = false - return - } - - for { - select { - case event := <-l.watcher.Events: - // a new rule json file has been created or updated - if event.Op&fsnotify.Write == fsnotify.Write { - if strings.HasSuffix(event.Name, ".json") { - log.Important("Ruleset changed due to %s, reloading ...", path.Base(event.Name)) - if err := l.loadRule(event.Name); err != nil { - log.Warning("%s", err) - } - } - } else if event.Op&fsnotify.Remove == fsnotify.Remove { - if strings.HasSuffix(event.Name, ".json") { - log.Important("Rule deleted %s", path.Base(event.Name)) - // we only need to delete from memory rules of type Always, - // because the Remove event is of a file, i.e.: Duration == Always - l.deleteRule(event.Name) - } - } - case err := <-l.watcher.Errors: - log.Error("File system watcher error: %s", err) - } - } -} - func (l *Loader) isTemporary(r *Rule) bool { return r.Duration != Restart && r.Duration != Always && r.Duration != Once } @@ -247,6 +282,18 @@ func (l *Loader) setUniqueName(rule *Rule) { } } +// Deprecated: rule.Operator.Data no longer holds the operator list in json format as string. +func (l *Loader) unmarshalOperatorList(op *Operator) error { + if op.Type == List && len(op.List) == 0 && op.Data != "" { + if err := json.Unmarshal([]byte(op.Data), &op.List); err != nil { + return fmt.Errorf("error loading rule of type list: %s", err) + } + op.Data = "" + } + + return nil +} + func (l *Loader) sortRules() { l.rulesKeys = make([]string, 0, len(l.rules)) for k := range l.rules { @@ -278,22 +325,21 @@ func (l *Loader) replaceUserRule(rule *Rule) (err error) { l.cleanListsRule(oldRule) } + if err := l.unmarshalOperatorList(&rule.Operator); err != nil { + log.Error(err.Error()) + } + if rule.Enabled { if err := rule.Operator.Compile(); err != nil { log.Warning("Operator.Compile() error: %s: %s", err, rule.Operator.Data) - return fmt.Errorf("(2) Error compiling rule: %s", err) + return fmt.Errorf("(2) error compiling rule: %s", err) } if rule.Operator.Type == List { - // TODO: use List protobuf object instead of un/marshalling to/from json - if err = json.Unmarshal([]byte(rule.Operator.Data), &rule.Operator.List); err != nil { - return fmt.Errorf("Error loading rule of type list: %s", err) - } - for i := 0; i < len(rule.Operator.List); i++ { if err := rule.Operator.List[i].Compile(); err != nil { log.Warning("Operator.Compile() error: %s: ", err) - return fmt.Errorf("(2) Error compiling list rule: %s", err) + return fmt.Errorf("(2) error compiling list rule: %s", err) } } } @@ -333,70 +379,39 @@ func (l *Loader) scheduleTemporaryRule(rule Rule) error { return nil } -// Add adds a rule to the list of rules, and optionally saves it to disk. -func (l *Loader) Add(rule *Rule, saveToDisk bool) error { - l.addUserRule(rule) - if saveToDisk { - fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) - return l.Save(rule, fileName) - } - return nil -} +func (l *Loader) liveReloadWorker() { + l.liveReloadRunning = true -// Replace adds a rule to the list of rules, and optionally saves it to disk. -func (l *Loader) Replace(rule *Rule, saveToDisk bool) error { - if err := l.replaceUserRule(rule); err != nil { - return err - } - if saveToDisk { - l.Lock() - defer l.Unlock() - - fileName := filepath.Join(l.path, fmt.Sprintf("%s.json", rule.Name)) - return l.Save(rule, fileName) - } - return nil -} - -// Save a rule to disk. -func (l *Loader) Save(rule *Rule, path string) error { - // When saving the rule, use always RFC3339 format for the Created field (#1140). - rule.Updated = time.Now().Format(time.RFC3339) - - raw, err := json.MarshalIndent(rule, "", " ") - if err != nil { - return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) + log.Debug("Rules watcher started on path %s ...", l.path) + if err := l.watcher.Add(l.path); err != nil { + log.Error("Could not watch path: %s", err) + l.liveReloadRunning = false + return } - if err = ioutil.WriteFile(path, raw, 0600); err != nil { - return fmt.Errorf("Error while saving rule %s to %s: %s", rule, path, err) + for { + select { + case event := <-l.watcher.Events: + // a new rule json file has been created or updated + if event.Op&fsnotify.Write == fsnotify.Write { + if strings.HasSuffix(event.Name, ".json") { + log.Important("Ruleset changed due to %s, reloading ...", path.Base(event.Name)) + if err := l.loadRule(event.Name); err != nil { + log.Warning("%s", err) + } + } + } else if event.Op&fsnotify.Remove == fsnotify.Remove { + if strings.HasSuffix(event.Name, ".json") { + log.Important("Rule deleted %s", path.Base(event.Name)) + // we only need to delete from memory rules of type Always, + // because the Remove event is of a file, i.e.: Duration == Always + l.deleteRule(event.Name) + } + } + case err := <-l.watcher.Errors: + log.Error("File system watcher error: %s", err) + } } - - return nil -} - -// Delete deletes a rule from the list by name. -// If the duration is Always (i.e: saved on disk), it'll attempt to delete -// it from disk. -func (l *Loader) Delete(ruleName string) error { - l.Lock() - defer l.Unlock() - - rule := l.rules[ruleName] - if rule == nil { - return nil - } - l.cleanListsRule(rule) - - delete(l.rules, ruleName) - l.sortRules() - - if rule.Duration != Always { - return nil - } - - log.Info("Delete() rule: %s", rule) - return l.deleteRuleFromDisk(ruleName) } // FindFirstMatch will try match the connection against the existing rule set. diff --git a/daemon/rule/loader_test.go b/daemon/rule/loader_test.go index 5ea32961..70497f0d 100644 --- a/daemon/rule/loader_test.go +++ b/daemon/rule/loader_test.go @@ -1,6 +1,7 @@ package rule import ( + "fmt" "io" "math/rand" "os" @@ -95,6 +96,57 @@ func TestRuleLoaderInvalidRegexp(t *testing.T) { }) } +// Test rules of type operator.list. There're these scenarios: +// - Enabled rules: +// * operator Data field is ignored if it contains the list of operators as json string. +// * the operarots list is expanded as json objecs under "list": [] +// For new rules (> v1.6.3), Data field will be empty. +// +// - Disabled rules +// * (old) the Data field contains the list of operators as json string, and the list of operarots is empty. +// * Data field empty, and the list of operators expanded. +// In all cases the list of operators must be loaded. +func TestRuleLoaderList(t *testing.T) { + l, err := NewLoader(true) + if err != nil { + t.Fail() + } + + testRules := map[string]string{ + "rule-with-operator-list": "testdata/rule-operator-list.json", + "rule-disabled-with-operators-list-as-json-string": "testdata/rule-disabled-operator-list.json", + "rule-disabled-with-operators-list-expanded": "testdata/rule-disabled-operator-list-expanded.json", + "rule-with-operator-list-data-empty": "testdata/rule-operator-list-data-empty.json", + } + + for name, path := range testRules { + t.Run(fmt.Sprint("loadRule() ", path), func(t *testing.T) { + if err := l.loadRule(path); err != nil { + t.Error(fmt.Sprint("loadRule() ", path, " error:"), err) + } + t.Log("Test: List rule:", name, path) + r, found := l.rules[name] + if !found { + t.Error(fmt.Sprint("loadRule() ", path, " not in the list:"), l.rules) + } + // Starting from > v1.6.3, after loading a rule of type List, the field Operator.Data is emptied, if the Data contained the list of operators as json. + if len(r.Operator.List) != 2 { + t.Error(fmt.Sprint("loadRule() ", path, " operator List not loaded:"), r) + } + if r.Operator.List[0].Type != Simple || + r.Operator.List[0].Operand != OpProcessPath || + r.Operator.List[0].Data != "/usr/bin/telnet" { + t.Error(fmt.Sprint("loadRule() ", path, " operator List 0 not loaded:"), r) + } + if r.Operator.List[1].Type != Simple || + r.Operator.List[1].Operand != OpDstPort || + r.Operator.List[1].Data != "53" { + t.Error(fmt.Sprint("loadRule() ", path, " operator List 1 not loaded:"), r) + } + }) + } +} + func TestLiveReload(t *testing.T) { t.Parallel() t.Log("Test rules loader with live reload") diff --git a/daemon/rule/rule.go b/daemon/rule/rule.go index de5bf495..034cdd82 100644 --- a/daemon/rule/rule.go +++ b/daemon/rule/rule.go @@ -63,7 +63,11 @@ func Create(name, description string, enabled, precedence, nolog bool, action Ac } func (r *Rule) String() string { - return fmt.Sprintf("%s: if(%s){ %s %s }", r.Name, r.Operator.String(), r.Action, r.Duration) + enabled := "Disabled" + if r.Enabled { + enabled = "Enabled" + } + return fmt.Sprintf("[%s] %s: if(%s){ %s %s }", enabled, r.Name, r.Operator.String(), r.Action, r.Duration) } // Match performs on a connection the checks a Rule has, to determine if it @@ -90,7 +94,7 @@ func Deserialize(reply *protocol.Rule) (*Rule, error) { return nil, err } - return Create( + newRule := Create( reply.Name, reply.Description, reply.Enabled, @@ -99,7 +103,24 @@ func Deserialize(reply *protocol.Rule) (*Rule, error) { Action(reply.Action), Duration(reply.Duration), operator, - ), nil + ) + + if Type(reply.Operator.Type) == List { + reply.Operator.Data = "" + for i := 0; i < len(reply.Operator.List); i++ { + newRule.Operator.List = append( + newRule.Operator.List, + Operator{ + Type: Type(reply.Operator.List[i].Type), + Sensitive: Sensitive(reply.Operator.List[i].Sensitive), + Operand: Operand(reply.Operator.List[i].Operand), + Data: string(reply.Operator.List[i].Data), + }, + ) + } + } + + return newRule, nil } // Serialize translates a Rule to the protocol object @@ -115,7 +136,7 @@ func (r *Rule) Serialize() *protocol.Rule { created = time.Now() } - return &protocol.Rule{ + protoRule := &protocol.Rule{ Created: created.Unix(), Name: string(r.Name), Description: string(r.Description), @@ -131,4 +152,18 @@ func (r *Rule) Serialize() *protocol.Rule { Data: string(r.Operator.Data), }, } + if r.Operator.Type == List { + r.Operator.Data = "" + for i := 0; i < len(r.Operator.List); i++ { + protoRule.Operator.List = append(protoRule.Operator.List, + &protocol.Operator{ + Type: string(r.Operator.List[i].Type), + Sensitive: bool(r.Operator.List[i].Sensitive), + Operand: string(r.Operator.List[i].Operand), + Data: string(r.Operator.List[i].Data), + }) + } + } + + return protoRule } diff --git a/daemon/rule/testdata/rule-disabled-operator-list-expanded.json b/daemon/rule/testdata/rule-disabled-operator-list-expanded.json new file mode 100644 index 00000000..2d6153f5 --- /dev/null +++ b/daemon/rule/testdata/rule-disabled-operator-list-expanded.json @@ -0,0 +1,31 @@ +{ + "created": "2023-10-03T18:06:52.209804547+01:00", + "updated": "2023-10-03T18:06:52.209857713+01:00", + "name": "rule-disabled-with-operators-list-expanded", + "enabled": false, + "precedence": true, + "action": "allow", + "duration": "always", + "operator": { + "type": "list", + "operand": "list", + "sensitive": false, + "data": "", + "list": [ + { + "type": "simple", + "operand": "process.path", + "sensitive": false, + "data": "/usr/bin/telnet", + "list": null + }, + { + "type": "simple", + "operand": "dest.port", + "sensitive": false, + "data": "53", + "list": null + } + ] + } +} diff --git a/daemon/rule/testdata/rule-disabled-operator-list.json b/daemon/rule/testdata/rule-disabled-operator-list.json new file mode 100644 index 00000000..29c8a7a7 --- /dev/null +++ b/daemon/rule/testdata/rule-disabled-operator-list.json @@ -0,0 +1,17 @@ +{ + "created": "2023-10-03T18:06:52.209804547+01:00", + "updated": "2023-10-03T18:06:52.209857713+01:00", + "name": "rule-disabled-with-operators-list-as-json-string", + "enabled": false, + "precedence": true, + "action": "allow", + "duration": "always", + "operator": { + "type": "list", + "operand": "list", + "sensitive": false, + "data": "[{\"type\": \"simple\", \"operand\": \"process.path\", \"sensitive\": false, \"data\": \"/usr/bin/telnet\"}, {\"type\": \"simple\", \"operand\": \"dest.port\", \"data\": \"53\", \"sensitive\": false}]", + "list": [ + ] + } +} diff --git a/daemon/rule/testdata/rule-operator-list-data-empty.json b/daemon/rule/testdata/rule-operator-list-data-empty.json new file mode 100644 index 00000000..76218c6a --- /dev/null +++ b/daemon/rule/testdata/rule-operator-list-data-empty.json @@ -0,0 +1,32 @@ +{ + "created": "2023-10-03T18:06:52.209804547+01:00", + "updated": "2023-10-03T18:06:52.209857713+01:00", + "name": "rule-with-operator-list-data-empty", + "enabled": true, + "precedence": true, + "action": "allow", + "duration": "always", + "operator": { + "type": "list", + "operand": "list", + "sensitive": false, + "data": "", + "list": [ + { + "type": "simple", + "operand": "process.path", + "sensitive": false, + "data": "/usr/bin/telnet", + "list": null + }, + { + "type": "simple", + "operand": "dest.port", + "sensitive": false, + "data": "53", + "list": null + } + ] + } +} + diff --git a/daemon/rule/testdata/rule-operator-list.json b/daemon/rule/testdata/rule-operator-list.json new file mode 100644 index 00000000..f4c46d8a --- /dev/null +++ b/daemon/rule/testdata/rule-operator-list.json @@ -0,0 +1,31 @@ +{ + "created": "2023-10-03T18:06:52.209804547+01:00", + "updated": "2023-10-03T18:06:52.209857713+01:00", + "name": "rule-with-operator-list", + "enabled": true, + "precedence": true, + "action": "allow", + "duration": "always", + "operator": { + "type": "list", + "operand": "list", + "sensitive": false, + "data": "[{\"type\": \"simple\", \"operand\": \"process.path\", \"sensitive\": false, \"data\": \"/usr/bin/telnet\"}, {\"type\": \"simple\", \"operand\": \"dest.port\", \"data\": \"53\", \"sensitive\": false}]", + "list": [ + { + "type": "simple", + "operand": "process.path", + "sensitive": false, + "data": "/usr/bin/telnet", + "list": null + }, + { + "type": "simple", + "operand": "dest.port", + "sensitive": false, + "data": "53", + "list": null + } + ] + } +} diff --git a/proto/ui.proto b/proto/ui.proto index 7826b1d7..2c5b3c08 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -140,6 +140,7 @@ message Operator { string operand = 2; string data = 3; bool sensitive = 4; + repeated Operator list = 5; } message Rule { diff --git a/ui/opensnitch/dialogs/prompt.py b/ui/opensnitch/dialogs/prompt.py index bae25204..e777c058 100644 --- a/ui/opensnitch/dialogs/prompt.py +++ b/ui/opensnitch/dialogs/prompt.py @@ -18,7 +18,7 @@ from opensnitch.desktop_parser import LinuxDesktopParser from opensnitch.config import Config from opensnitch.version import version from opensnitch.actions import Actions -from opensnitch.rules import Rules +from opensnitch.rules import Rules, Rule from opensnitch import ui_pb2 @@ -685,10 +685,25 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): data.append({"type": Config.RULE_TYPE_SIMPLE, "operand": Config.OPERAND_PROCESS_PATH, "data": str(self._con.process_path)}) if is_list_rule: - data.append({"type": self._rule.operator.type, "operand": self._rule.operator.operand, "data": self._rule.operator.data}) + data.append({ + "type": self._rule.operator.type, + "operand": self._rule.operator.operand, + "data": self._rule.operator.data + }) + # We need to send back the operator list to the AskRule() call + # as json string, in order to add it to the DB. self._rule.operator.data = json.dumps(data) self._rule.operator.type = Config.RULE_TYPE_LIST self._rule.operator.operand = Config.RULE_TYPE_LIST + for op in data: + self._rule.operator.list.extend([ + ui_pb2.Operator( + type=op['type'], + operand=op['operand'], + sensitive=False if op.get('sensitive') == None else op['sensitive'], + data="" if op.get('data') == None else op['data'] + ) + ]) exists = self._rules.exists(self._rule, self._peer) if not exists: diff --git a/ui/opensnitch/dialogs/ruleseditor.py b/ui/opensnitch/dialogs/ruleseditor.py index c16ab230..01b5ad48 100644 --- a/ui/opensnitch/dialogs/ruleseditor.py +++ b/ui/opensnitch/dialogs/ruleseditor.py @@ -4,7 +4,6 @@ from PyQt5.QtCore import QCoreApplication as QC from slugify import slugify from datetime import datetime import re -import json import sys import os import pwd @@ -434,13 +433,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): if self.rule.operator.type != Config.RULE_TYPE_LIST: self._load_rule_operator(self.rule.operator) else: - rule_options = json.loads(self.rule.operator.data) - for r in rule_options: - _sensitive = False - if 'sensitive' in r: - _sensitive = r['sensitive'] - - op = ui_pb2.Operator(type=r['type'], operand=r['operand'], data=r['data'], sensitive=_sensitive) + for op in self.rule.operator.list: self._load_rule_operator(op) return True @@ -922,7 +915,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): 'data': self.dstListsLine.text(), 'sensitive': self.sensitiveCheck.isChecked() }) - self.rule.operator.data = json.dumps(rule_data) + self.rule.operator.data = "" if self.dstListRegexpCheck.isChecked(): if self.dstRegexpListsLine.text() == "": @@ -939,7 +932,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): 'data': self.dstRegexpListsLine.text(), 'sensitive': self.sensitiveCheck.isChecked() }) - self.rule.operator.data = json.dumps(rule_data) + self.rule.operator.data = "" if self.dstListNetsCheck.isChecked(): if self.dstListNetsLine.text() == "": @@ -956,7 +949,7 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): 'data': self.dstListNetsLine.text(), 'sensitive': self.sensitiveCheck.isChecked() }) - self.rule.operator.data = json.dumps(rule_data) + self.rule.operator.data = "" if self.dstListIPsCheck.isChecked(): @@ -974,12 +967,22 @@ class RulesEditorDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): 'data': self.dstListIPsLine.text(), 'sensitive': self.sensitiveCheck.isChecked() }) - self.rule.operator.data = json.dumps(rule_data) + self.rule.operator.data = "" if len(rule_data) >= 2: self.rule.operator.type = Config.RULE_TYPE_LIST self.rule.operator.operand = Config.RULE_TYPE_LIST - self.rule.operator.data = json.dumps(rule_data) + self.rule.operator.data = "" + for rd in rule_data: + self.rule.operator.list.extend([ + ui_pb2.Operator( + type=rd['type'], + operand=rd['operand'], + data=rd['data'], + sensitive=rd['sensitive'] + ) + ]) + print(self.rule.operator.list) elif len(rule_data) == 1: self.rule.operator.operand = rule_data[0]['operand'] diff --git a/ui/opensnitch/rules.py b/ui/opensnitch/rules.py index 8150b8ad..c0fb6510 100644 --- a/ui/opensnitch/rules.py +++ b/ui/opensnitch/rules.py @@ -51,6 +51,25 @@ class Rule(): ).timestamp()) rule.created = created + try: + # Operator list is always saved as json string to the db, + # so we need to load the json string. + if rule.operator.type == Config.RULE_TYPE_LIST: + operators = json.loads(rule.operator.data) + for op in operators: + rule.operator.list.extend([ + ui_pb2.Operator( + type=op['type'], + operand=op['operand'], + sensitive=False if op.get('sensitive') == None else op['sensitive'], + data="" if op.get('data') == None else op['data'] + ) + ]) + rule.operator.data = "" + except Exception as e: + print("new_from_records exception parsing operartor list:", e) + + return rule class Rules(QObject): @@ -83,6 +102,11 @@ class Rules(QObject): def add_rules(self, addr, rules): try: for _,r in enumerate(rules): + # Operator list is always saved as json string to the db. + rjson = json.loads(MessageToJson(r)) + if r.operator.type == Config.RULE_TYPE_LIST and rjson.get('operator') != None and rjson.get('operator').get('list') != None: + r.operator.data = json.dumps(rjson.get('operator').get('list')) + self.add(datetime.now().strftime(DBDateFieldFormat), addr, r.name, r.description, str(r.enabled), diff --git a/ui/opensnitch/service.py b/ui/opensnitch/service.py index c5560719..c14e6bd0 100644 --- a/ui/opensnitch/service.py +++ b/ui/opensnitch/service.py @@ -696,6 +696,10 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject): rule.operator.data, str(datetime.fromtimestamp(rule.created).strftime("%Y-%m-%d %H:%M:%S")) ) + if rule.operator.type == Config.RULE_TYPE_LIST: + # reset list operator data before sending it back to the + # daemon. + rule.operator.data = "" elif kwargs['action'] == self.DELETE_RULE: self._db.delete_rule(kwargs['name'], kwargs['addr'])