2020-12-19 19:31:09 +01:00
|
|
|
package rule
|
|
|
|
|
|
|
|
import (
|
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
2023-10-09 14:55:15 +02:00
|
|
|
"fmt"
|
2020-12-28 11:59:29 +00:00
|
|
|
"io"
|
|
|
|
"math/rand"
|
|
|
|
"os"
|
2020-12-19 19:31:09 +01:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2020-12-28 11:59:29 +00:00
|
|
|
var tmpDir string
|
|
|
|
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
tmpDir = "/tmp/ostest_" + randString()
|
|
|
|
os.Mkdir(tmpDir, 0777)
|
|
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
os.Exit(m.Run())
|
|
|
|
}
|
2021-11-12 12:08:31 +01:00
|
|
|
|
2020-12-19 19:31:09 +01:00
|
|
|
func TestRuleLoader(t *testing.T) {
|
2020-12-28 11:59:29 +00:00
|
|
|
t.Parallel()
|
2020-12-19 19:31:09 +01:00
|
|
|
t.Log("Test rules loader")
|
|
|
|
|
|
|
|
var list []Operator
|
|
|
|
dur1s := Duration("1s")
|
|
|
|
dummyOper, _ := NewOperator(Simple, false, OpTrue, "", list)
|
2021-02-27 01:56:49 +01:00
|
|
|
dummyOper.Compile()
|
2022-07-04 11:14:26 +02:00
|
|
|
inMem1sRule := Create("000-xxx-name", "rule description xxx", true, false, false, Allow, dur1s, dummyOper)
|
|
|
|
inMemUntilRestartRule := Create("000-aaa-name", "rule description aaa", true, false, false, Allow, Restart, dummyOper)
|
2020-12-19 19:31:09 +01:00
|
|
|
|
|
|
|
l, err := NewLoader(false)
|
|
|
|
if err != nil {
|
|
|
|
t.Fail()
|
|
|
|
}
|
|
|
|
if err = l.Load("/non/existent/path/"); err == nil {
|
|
|
|
t.Error("non existent path test: err should not be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = l.Load("testdata/"); err != nil {
|
|
|
|
t.Error("Error loading test rules: ", err)
|
|
|
|
}
|
2023-10-09 18:07:48 +02:00
|
|
|
// we expect 6 valid rules (2 invalid), loaded from testdata/
|
|
|
|
testNumRules(t, l, 6)
|
2020-12-19 19:31:09 +01:00
|
|
|
|
|
|
|
if err = l.Add(inMem1sRule, false); err != nil {
|
|
|
|
t.Error("Error adding temporary rule")
|
|
|
|
}
|
2023-10-09 18:07:48 +02:00
|
|
|
testNumRules(t, l, 7)
|
2020-12-19 19:31:09 +01:00
|
|
|
|
|
|
|
// test auto deletion of temporary rule
|
|
|
|
time.Sleep(time.Second * 2)
|
2023-10-09 18:07:48 +02:00
|
|
|
testNumRules(t, l, 6)
|
2020-12-19 19:31:09 +01:00
|
|
|
|
|
|
|
if err = l.Add(inMemUntilRestartRule, false); err != nil {
|
|
|
|
t.Error("Error adding temporary rule (2)")
|
|
|
|
}
|
2023-10-09 18:07:48 +02:00
|
|
|
testNumRules(t, l, 7)
|
2020-12-19 19:31:09 +01:00
|
|
|
testRulesOrder(t, l)
|
|
|
|
testSortRules(t, l)
|
|
|
|
testFindMatch(t, l)
|
|
|
|
testFindEnabled(t, l)
|
2021-06-03 17:35:48 +02:00
|
|
|
testDurationChange(t, l)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
2021-11-12 12:08:31 +01:00
|
|
|
|
|
|
|
func TestRuleLoaderInvalidRegexp(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
t.Log("Test rules loader: invalid regexp")
|
|
|
|
|
|
|
|
l, err := NewLoader(true)
|
|
|
|
if err != nil {
|
|
|
|
t.Fail()
|
|
|
|
}
|
|
|
|
t.Run("loadRule() from disk test (simple)", func(t *testing.T) {
|
|
|
|
if err := l.loadRule("testdata/invalid-regexp.json"); err == nil {
|
|
|
|
t.Error("invalid regexp rule loaded: loadRule()")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("loadRule() from disk test (list)", func(t *testing.T) {
|
|
|
|
if err := l.loadRule("testdata/invalid-regexp-list.json"); err == nil {
|
|
|
|
t.Error("invalid regexp rule loaded: loadRule()")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
var list []Operator
|
|
|
|
dur30m := Duration("30m")
|
|
|
|
opListData := `[{"type": "regexp", "operand": "process.path", "sensitive": false, "data": "^(/di(rmngr)$"}, {"type": "simple", "operand": "dest.port", "data": "53", "sensitive": false}]`
|
|
|
|
invalidRegexpOp, _ := NewOperator(List, false, OpList, opListData, list)
|
2022-07-04 11:14:26 +02:00
|
|
|
invalidRegexpRule := Create("invalid-regexp", "invalid rule description", true, false, false, Allow, dur30m, invalidRegexpOp)
|
2021-11-12 12:08:31 +01:00
|
|
|
|
|
|
|
t.Run("replaceUserRule() test list", func(t *testing.T) {
|
|
|
|
if err := l.replaceUserRule(invalidRegexpRule); err == nil {
|
|
|
|
t.Error("invalid regexp rule loaded: replaceUserRule()")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
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
2023-10-09 14:55:15 +02:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-12-19 19:31:09 +01:00
|
|
|
|
2020-12-28 11:59:29 +00:00
|
|
|
func TestLiveReload(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
t.Log("Test rules loader with live reload")
|
|
|
|
l, err := NewLoader(true)
|
|
|
|
if err != nil {
|
|
|
|
t.Fail()
|
|
|
|
}
|
|
|
|
if err = Copy("testdata/000-allow-chrome.json", tmpDir+"/000-allow-chrome.json"); err != nil {
|
|
|
|
t.Error("Error copying rule into a temp dir")
|
|
|
|
}
|
|
|
|
if err = Copy("testdata/001-deny-chrome.json", tmpDir+"/001-deny-chrome.json"); err != nil {
|
|
|
|
t.Error("Error copying rule into a temp dir")
|
|
|
|
}
|
|
|
|
if err = l.Load(tmpDir); err != nil {
|
|
|
|
t.Error("Error loading test rules: ", err)
|
|
|
|
}
|
|
|
|
//wait for watcher to activate
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
if err = Copy("testdata/live_reload/test-live-reload-remove.json", tmpDir+"/test-live-reload-remove.json"); err != nil {
|
|
|
|
t.Error("Error copying rules into temp dir")
|
|
|
|
}
|
|
|
|
if err = Copy("testdata/live_reload/test-live-reload-delete.json", tmpDir+"/test-live-reload-delete.json"); err != nil {
|
|
|
|
t.Error("Error copying rules into temp dir")
|
|
|
|
}
|
|
|
|
//wait for watcher to pick up the changes
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
testNumRules(t, l, 4)
|
|
|
|
if err = os.Remove(tmpDir + "/test-live-reload-remove.json"); err != nil {
|
|
|
|
t.Error("Error Remove()ing file from temp dir")
|
|
|
|
}
|
|
|
|
if err = l.Delete("test-live-reload-delete"); err != nil {
|
|
|
|
t.Error("Error Delete()ing file from temp dir")
|
|
|
|
}
|
|
|
|
//wait for watcher to pick up the changes
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
testNumRules(t, l, 2)
|
|
|
|
}
|
|
|
|
|
|
|
|
func randString() string {
|
|
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
|
|
b := make([]rune, 10)
|
|
|
|
for i := range b {
|
|
|
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
|
|
|
}
|
|
|
|
return string(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
func Copy(src, dst string) error {
|
|
|
|
in, err := os.Open(src)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer in.Close()
|
|
|
|
|
|
|
|
out, err := os.Create(dst)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer out.Close()
|
|
|
|
|
|
|
|
_, err = io.Copy(out, in)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return out.Close()
|
|
|
|
}
|
|
|
|
|
2020-12-19 19:31:09 +01:00
|
|
|
func testNumRules(t *testing.T, l *Loader, num int) {
|
|
|
|
if l.NumRules() != num {
|
|
|
|
t.Error("rules number should be (2): ", num)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testRulesOrder(t *testing.T, l *Loader) {
|
2025-02-08 15:12:11 +01:00
|
|
|
if l.activeRules[0] != "000-aaa-name" {
|
|
|
|
t.Error("Rules not in order (0): ", l.activeRules)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
2025-02-08 15:12:11 +01:00
|
|
|
if l.activeRules[1] != "000-allow-chrome" {
|
|
|
|
t.Error("Rules not in order (1): ", l.activeRules)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
2025-02-08 15:12:11 +01:00
|
|
|
if l.activeRules[2] != "001-deny-chrome" {
|
|
|
|
t.Error("Rules not in order (2): ", l.activeRules)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testSortRules(t *testing.T, l *Loader) {
|
2025-02-08 15:12:11 +01:00
|
|
|
l.activeRules[1] = "001-deny-chrome"
|
|
|
|
l.activeRules[2] = "000-allow-chrome"
|
2020-12-19 19:31:09 +01:00
|
|
|
l.sortRules()
|
2025-02-08 15:12:11 +01:00
|
|
|
if l.activeRules[1] != "000-allow-chrome" {
|
|
|
|
t.Error("Rules not in order (1): ", l.activeRules)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
2025-02-08 15:12:11 +01:00
|
|
|
if l.activeRules[2] != "001-deny-chrome" {
|
|
|
|
t.Error("Rules not in order (2): ", l.activeRules)
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFindMatch(t *testing.T, l *Loader) {
|
|
|
|
conn.Process.Path = "/opt/google/chrome/chrome"
|
|
|
|
|
|
|
|
testFindPriorityMatch(t, l)
|
|
|
|
testFindDenyMatch(t, l)
|
|
|
|
testFindAllowMatch(t, l)
|
|
|
|
|
|
|
|
restoreConnection()
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFindPriorityMatch(t *testing.T, l *Loader) {
|
|
|
|
match := l.FindFirstMatch(conn)
|
|
|
|
if match == nil {
|
|
|
|
t.Error("FindPriorityMatch didn't match")
|
|
|
|
}
|
|
|
|
// test 000-allow-chrome, priority == true
|
|
|
|
if match.Name != "000-allow-chrome" {
|
|
|
|
t.Error("findPriorityMatch: priority rule failed: ", match)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFindDenyMatch(t *testing.T, l *Loader) {
|
|
|
|
l.rules["000-allow-chrome"].Precedence = false
|
|
|
|
// test 000-allow-chrome, priority == false
|
|
|
|
// 001-deny-chrome must match
|
|
|
|
match := l.FindFirstMatch(conn)
|
|
|
|
if match == nil {
|
|
|
|
t.Error("FindDenyMatch deny didn't match")
|
|
|
|
}
|
|
|
|
if match.Name != "001-deny-chrome" {
|
|
|
|
t.Error("findDenyMatch: deny rule failed: ", match)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFindAllowMatch(t *testing.T, l *Loader) {
|
|
|
|
l.rules["000-allow-chrome"].Precedence = false
|
|
|
|
l.rules["001-deny-chrome"].Action = Allow
|
|
|
|
// test 000-allow-chrome, priority == false
|
|
|
|
// 001-deny-chrome must match
|
|
|
|
match := l.FindFirstMatch(conn)
|
|
|
|
if match == nil {
|
|
|
|
t.Error("FindAllowMatch allow didn't match")
|
|
|
|
}
|
|
|
|
if match.Name != "001-deny-chrome" {
|
|
|
|
t.Error("findAllowMatch: allow rule failed: ", match)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testFindEnabled(t *testing.T, l *Loader) {
|
|
|
|
l.rules["000-allow-chrome"].Precedence = false
|
|
|
|
l.rules["001-deny-chrome"].Action = Allow
|
|
|
|
l.rules["001-deny-chrome"].Enabled = false
|
|
|
|
// test 000-allow-chrome, priority == false
|
|
|
|
// 001-deny-chrome must match
|
|
|
|
match := l.FindFirstMatch(conn)
|
|
|
|
if match == nil {
|
|
|
|
t.Error("FindEnabledMatch, match nil")
|
|
|
|
}
|
|
|
|
if match.Name == "001-deny-chrome" {
|
2021-06-03 17:35:48 +02:00
|
|
|
t.Error("findEnabledMatch: deny rule shouldn't have matched: ", match)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// test that changing the Duration of a temporary rule doesn't delete
|
|
|
|
// the new one, ignoring the old timer.
|
|
|
|
func testDurationChange(t *testing.T, l *Loader) {
|
|
|
|
l.rules["000-aaa-name"].Duration = "2s"
|
|
|
|
if err := l.replaceUserRule(l.rules["000-aaa-name"]); err != nil {
|
|
|
|
t.Error("testDurationChange, error replacing rule: ", err)
|
|
|
|
}
|
|
|
|
l.rules["000-aaa-name"].Duration = "1h"
|
|
|
|
if err := l.replaceUserRule(l.rules["000-aaa-name"]); err != nil {
|
|
|
|
t.Error("testDurationChange, error replacing rule: ", err)
|
|
|
|
}
|
|
|
|
time.Sleep(time.Second * 4)
|
|
|
|
if _, found := l.rules["000-aaa-name"]; !found {
|
|
|
|
t.Error("testDurationChange, error: rule has been deleted")
|
2020-12-19 19:31:09 +01:00
|
|
|
}
|
|
|
|
}
|