123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- package csplugin
- import (
- "encoding/json"
- "io/ioutil"
- "os"
- "os/exec"
- "path"
- "reflect"
- "testing"
- "time"
- log "github.com/sirupsen/logrus"
- "github.com/crowdsecurity/crowdsec/pkg/csconfig"
- "github.com/crowdsecurity/crowdsec/pkg/models"
- "github.com/pkg/errors"
- "github.com/stretchr/testify/assert"
- "gopkg.in/tomb.v2"
- "gopkg.in/yaml.v2"
- )
- var testPath string
- func setPluginPermTo744() {
- setPluginPermTo("744")
- }
- func setPluginPermTo722() {
- setPluginPermTo("722")
- }
- func setPluginPermTo724() {
- setPluginPermTo("724")
- }
- func TestGetPluginNameAndTypeFromPath(t *testing.T) {
- setUp()
- defer tearDown()
- type args struct {
- path string
- }
- tests := []struct {
- name string
- args args
- want string
- want1 string
- wantErr bool
- }{
- {
- name: "valid plugin name, single dash",
- args: args{
- path: path.Join(testPath, "notification-gitter"),
- },
- want: "notification",
- want1: "gitter",
- wantErr: false,
- },
- {
- name: "invalid plugin name",
- args: args{
- path: "./tests/gitter",
- },
- want: "",
- want1: "",
- wantErr: true,
- },
- {
- name: "valid plugin name, multiple dash",
- args: args{
- path: "./tests/notification-instant-slack",
- },
- want: "notification-instant",
- want1: "slack",
- wantErr: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, got1, err := getPluginTypeAndSubtypeFromPath(tt.args.path)
- if (err != nil) != tt.wantErr {
- t.Errorf("getPluginNameAndTypeFromPath() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if got != tt.want {
- t.Errorf("getPluginNameAndTypeFromPath() got = %v, want %v", got, tt.want)
- }
- if got1 != tt.want1 {
- t.Errorf("getPluginNameAndTypeFromPath() got1 = %v, want %v", got1, tt.want1)
- }
- })
- }
- }
- func TestListFilesAtPath(t *testing.T) {
- setUp()
- defer tearDown()
- type args struct {
- path string
- }
- tests := []struct {
- name string
- args args
- want []string
- wantErr bool
- }{
- {
- name: "valid directory",
- args: args{
- path: testPath,
- },
- want: []string{
- path.Join(testPath, "notification-gitter"),
- path.Join(testPath, "slack"),
- },
- },
- {
- name: "invalid directory",
- args: args{
- path: "./foo/bar/",
- },
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := listFilesAtPath(tt.args.path)
- if (err != nil) != tt.wantErr {
- t.Errorf("listFilesAtPath() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("listFilesAtPath() = %v, want %v", got, tt.want)
- }
- })
- }
- }
- func TestBrokerInit(t *testing.T) {
- tests := []struct {
- name string
- action func()
- errContains string
- wantErr bool
- procCfg csconfig.PluginCfg
- }{
- {
- name: "valid config",
- action: setPluginPermTo744,
- wantErr: false,
- },
- {
- name: "group writable binary",
- wantErr: true,
- errContains: "notification-dummy is world writable",
- action: setPluginPermTo722,
- },
- {
- name: "group writable binary",
- wantErr: true,
- errContains: "notification-dummy is group writable",
- action: setPluginPermTo724,
- },
- {
- name: "no plugin dir",
- wantErr: true,
- errContains: "no such file or directory",
- action: tearDown,
- },
- {
- name: "no plugin binary",
- wantErr: true,
- errContains: "binary for plugin dummy_default not found",
- action: func() {
- err := os.Remove(path.Join(testPath, "notification-dummy"))
- if err != nil {
- t.Fatal(err)
- }
- },
- },
- {
- name: "only specify user",
- wantErr: true,
- errContains: "both plugin user and group must be set",
- procCfg: csconfig.PluginCfg{
- User: "123445555551122toto",
- },
- action: setPluginPermTo744,
- },
- {
- name: "only specify group",
- wantErr: true,
- errContains: "both plugin user and group must be set",
- procCfg: csconfig.PluginCfg{
- Group: "123445555551122toto",
- },
- action: setPluginPermTo744,
- },
- {
- name: "Fails to run as root",
- wantErr: true,
- errContains: "operation not permitted",
- procCfg: csconfig.PluginCfg{
- User: "root",
- Group: "root",
- },
- action: setPluginPermTo744,
- },
- {
- name: "Invalid user and group",
- wantErr: true,
- errContains: "unknown user toto1234",
- procCfg: csconfig.PluginCfg{
- User: "toto1234",
- Group: "toto1234",
- },
- action: setPluginPermTo744,
- },
- {
- name: "Valid user and invalid group",
- wantErr: true,
- errContains: "unknown group toto1234",
- procCfg: csconfig.PluginCfg{
- User: "nobody",
- Group: "toto1234",
- },
- action: setPluginPermTo744,
- },
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- defer tearDown()
- buildDummyPlugin()
- if test.action != nil {
- test.action()
- }
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- err := pb.Init(&test.procCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- defer pb.Kill()
- if test.wantErr {
- assert.ErrorContains(t, err, test.errContains)
- } else {
- assert.NoError(t, err)
- }
- })
- }
- }
- func readconfig(t *testing.T, path string) ([]byte, PluginConfig) {
- var config PluginConfig
- orig, err := ioutil.ReadFile("tests/notifications/dummy.yaml")
- if err != nil {
- t.Fatalf("unable to read config file %s : %s", path, err)
- }
- if err := yaml.Unmarshal(orig, &config); err != nil {
- t.Fatalf("unable to unmarshal config file : %s", err)
- }
- return orig, config
- }
- func writeconfig(t *testing.T, config PluginConfig, path string) {
- data, err := yaml.Marshal(&config)
- if err != nil {
- t.Fatalf("unable to marshal config file : %s", err)
- }
- if err := ioutil.WriteFile(path, data, 0644); err != nil {
- t.Fatalf("unable to write config file %s : %s", path, err)
- }
- }
- func TestBrokerNoThreshold(t *testing.T) {
- var alerts []models.Alert
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- //init
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- //default config
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- //send one item, it should be processed right now
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(200 * time.Millisecond)
- //we expect one now
- content, err := ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 1, len(alerts))
- //remove it
- os.Remove("./out")
- //and another one
- log.Printf("second send")
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(200 * time.Millisecond)
- //we expect one again, as we cleaned the file
- content, err = ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- err = json.Unmarshal(content, &alerts)
- log.Printf("content-> %s", content)
- assert.NoError(t, err)
- assert.Equal(t, 1, len(alerts))
- }
- func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) {
- //test grouping by "time"
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- //init
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- //set groupwait and groupthreshold, should honor whichever comes first
- raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
- cfg.GroupThreshold = 4
- cfg.GroupWait = 1 * time.Second
- writeconfig(t, cfg, "tests/notifications/dummy.yaml")
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- //send data
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(500 * time.Millisecond)
- //because of group threshold, we shouldn't have data yet
- assert.NoFileExists(t, "./out")
- time.Sleep(1 * time.Second)
- //after 1 seconds, we should have data
- content, err := ioutil.ReadFile("./out")
- assert.NoError(t, err)
- var alerts []models.Alert
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 3, len(alerts))
- //restore config
- if err := ioutil.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
- t.Fatalf("unable to write config file %s", err)
- }
- }
- func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) {
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- //init
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- //set groupwait and groupthreshold, should honor whichever comes first
- raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
- cfg.GroupThreshold = 4
- cfg.GroupWait = 4 * time.Second
- writeconfig(t, cfg, "tests/notifications/dummy.yaml")
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- //send data
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(100 * time.Millisecond)
- //because of group threshold, we shouldn't have data yet
- assert.NoFileExists(t, "./out")
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(100 * time.Millisecond)
- //and now we should
- content, err := ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- var alerts []models.Alert
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 4, len(alerts))
- //restore config
- if err := ioutil.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
- t.Fatalf("unable to write config file %s", err)
- }
- }
- func TestBrokerRunGroupThreshold(t *testing.T) {
- //test grouping by "size"
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- //init
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- //set groupwait
- raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
- cfg.GroupThreshold = 4
- writeconfig(t, cfg, "tests/notifications/dummy.yaml")
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- //send data
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(100 * time.Millisecond)
- //because of group threshold, we shouldn't have data yet
- assert.NoFileExists(t, "./out")
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(100 * time.Millisecond)
- //and now we should
- content, err := ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- var alerts []models.Alert
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 4, len(alerts))
- //restore config
- if err := ioutil.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
- t.Fatalf("unable to write config file %s", err)
- }
- }
- func TestBrokerRunTimeThreshold(t *testing.T) {
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- //init
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- //set groupwait
- raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
- cfg.GroupWait = time.Duration(1 * time.Second)
- writeconfig(t, cfg, "tests/notifications/dummy.yaml")
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- //send data
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(200 * time.Millisecond)
- //we shouldn't have data yet
- assert.NoFileExists(t, "./out")
- time.Sleep(1 * time.Second)
- //and now we should
- content, err := ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- var alerts []models.Alert
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 1, len(alerts))
- //restore config
- if err := ioutil.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
- t.Fatalf("unable to write config file %s", err)
- }
- }
- func TestBrokerRunSimple(t *testing.T) {
- DefaultEmptyTicker = 50 * time.Millisecond
- buildDummyPlugin()
- setPluginPermTo744()
- defer tearDown()
- pluginCfg := csconfig.PluginCfg{}
- pb := PluginBroker{}
- profiles := csconfig.NewDefaultConfig().API.Server.Profiles
- profiles = append(profiles, &csconfig.ProfileCfg{
- Notifications: []string{"dummy_default"},
- })
- err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
- PluginDir: testPath,
- NotificationDir: "./tests/notifications",
- })
- assert.NoError(t, err)
- tomb := tomb.Tomb{}
- go pb.Run(&tomb)
- defer pb.Kill()
- assert.NoFileExists(t, "./out")
- defer os.Remove("./out")
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
- time.Sleep(time.Millisecond * 200)
- content, err := ioutil.ReadFile("./out")
- if err != nil {
- log.Errorf("Error reading file: %s", err)
- }
- var alerts []models.Alert
- err = json.Unmarshal(content, &alerts)
- assert.NoError(t, err)
- assert.Equal(t, 2, len(alerts))
- }
- func buildDummyPlugin() {
- dir, err := os.MkdirTemp("./tests", "cs_plugin_test")
- if err != nil {
- log.Fatal(err)
- }
- cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/")
- if err := cmd.Run(); err != nil {
- log.Fatal(err)
- }
- testPath = dir
- os.Remove("./out")
- }
- func setPluginPermTo(perm string) {
- if err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run(); err != nil {
- log.Fatal(errors.Wrapf(err, "chmod 744 %s", path.Join(testPath, "notification-dummy")))
- }
- }
- func setUp() {
- dir, err := os.MkdirTemp("./", "cs_plugin_test")
- if err != nil {
- log.Fatal(err)
- }
- _, err = os.Create(path.Join(dir, "slack"))
- if err != nil {
- log.Fatal(err)
- }
- _, err = os.Create(path.Join(dir, "notification-gitter"))
- if err != nil {
- log.Fatal(err)
- }
- err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666)
- if err != nil {
- log.Fatal(err)
- }
- testPath = dir
- }
- func tearDown() {
- err := os.RemoveAll(testPath)
- if err != nil {
- log.Fatal(err)
- }
- os.Remove("./out")
- }
|