CI: refactoring pkg/csplugin tests (#2247)

This commit is contained in:
mmetc 2023-06-01 10:33:08 +02:00 committed by GitHub
parent f6544962ea
commit 12c32d507c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 449 additions and 550 deletions

2
go.mod
View file

@ -71,7 +71,7 @@ require (
github.com/bluele/gcache v0.0.2 github.com/bluele/gcache v0.0.2
github.com/cespare/xxhash/v2 v2.1.2 github.com/cespare/xxhash/v2 v2.1.2
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd
github.com/goccy/go-yaml v1.9.7 github.com/goccy/go-yaml v1.9.7
github.com/gofrs/uuid v4.0.0+incompatible github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt/v4 v4.2.0 github.com/golang-jwt/jwt/v4 v4.2.0

4
go.sum
View file

@ -172,8 +172,8 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU= github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk= github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee h1:YD/SYJ0otjlyAxuLRzJNXTQyXcpccefTvaHextBT9mQ= github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd h1:Y70ceDKAKYFXTnxEjXuBDSh07umvDhbX3PCCYhdtsZ0=
github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee/go.mod h1:9JJLSpGj1ZXnROV3xAcJvS/HTaUvuA8K3gGOpO4tfVc= github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd/go.mod h1:9JJLSpGj1ZXnROV3xAcJvS/HTaUvuA8K3gGOpO4tfVc=
github.com/crowdsecurity/grokky v0.2.1 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4= github.com/crowdsecurity/grokky v0.2.1 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4=
github.com/crowdsecurity/grokky v0.2.1/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM= github.com/crowdsecurity/grokky v0.2.1/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM=
github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc= github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc=

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"text/template" "text/template"
@ -375,22 +374,6 @@ func setRequiredFields(pluginCfg *PluginConfig) {
} }
// helper which gives paths to all files in the given directory non-recursively
func listFilesAtPath(path string) ([]string, error) {
filePaths := make([]string, 0)
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
for _, file := range files {
if file.IsDir() {
continue
}
filePaths = append(filePaths, filepath.Join(path, file.Name()))
}
return filePaths, nil
}
func getUUID() (string, error) { func getUUID() (string, error) {
uuidv4, err := uuid.NewRandom() uuidv4, err := uuid.NewRandom()
if err != nil { if err != nil {

View file

@ -0,0 +1,164 @@
package csplugin
import (
"io"
"os"
"os/exec"
"path"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
)
type PluginSuite struct {
suite.Suite
// where the plugin is built - temporary directory for the suite
buildDir string
// full path to the built plugin binary
builtBinary string
runDir string // temporary directory for each test
pluginDir string // (config_paths.plugin_dir)
notifDir string // (config_paths.notification_dir)
pluginBinary string // full path to the plugin binary (unique for each test)
pluginConfig string // full path to the notification config (unique for each test)
pluginBroker *PluginBroker
}
func TestPluginSuite(t *testing.T) {
suite.Run(t, new(PluginSuite))
}
func (s *PluginSuite) SetupSuite() {
var err error
t := s.T()
s.buildDir, err = os.MkdirTemp("", "cs_plugin_test_build")
require.NoError(t, err)
s.builtBinary = path.Join(s.buildDir, "notification-dummy")
if runtime.GOOS == "windows" {
s.builtBinary += ".exe"
}
cmd := exec.Command("go", "build", "-o", s.builtBinary, "../../plugins/notifications/dummy/")
err = cmd.Run()
require.NoError(t, err, "while building dummy plugin")
}
func (s *PluginSuite) TearDownSuite() {
t := s.T()
err := os.RemoveAll(s.buildDir)
require.NoError(t, err)
}
func copyFile(src string, dst string) error {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(dst)
if err != nil {
return err
}
defer d.Close()
_, err = io.Copy(d, s)
if err != nil {
return err
}
err = d.Sync()
if err != nil {
return err
}
return nil
}
func (s *PluginSuite) SetupTest() {
s.SetupSubTest()
}
func (s *PluginSuite) TearDownTest() {
s.TearDownSubTest()
}
func (s *PluginSuite) SetupSubTest() {
var err error
t := s.T()
s.runDir, err = os.MkdirTemp("", "cs_plugin_test")
require.NoError(t, err)
s.pluginDir = path.Join(s.runDir, "bin")
err = os.MkdirAll(path.Join(s.runDir, "bin"), 0o755)
require.NoError(t, err, "while creating bin dir")
s.notifDir = path.Join(s.runDir, "config")
err = os.MkdirAll(s.notifDir, 0o755)
require.NoError(t, err, "while creating config dir")
s.pluginBinary = path.Join(s.pluginDir, "notification-dummy")
if runtime.GOOS == "windows" {
s.pluginBinary += ".exe"
}
err = copyFile(s.builtBinary, s.pluginBinary)
require.NoError(t, err, "while copying built binary")
err = os.Chmod(s.pluginBinary, 0o744)
require.NoError(t, err, "chmod 0744 %s", s.pluginBinary)
s.pluginConfig = path.Join(s.notifDir, "dummy.yaml")
err = copyFile("testdata/dummy.yaml", s.pluginConfig)
require.NoError(t, err, "while copying plugin config")
}
func (s *PluginSuite) TearDownSubTest() {
t := s.T()
if s.pluginBroker != nil {
s.pluginBroker.Kill()
s.pluginBroker = nil
}
err := os.RemoveAll(s.runDir)
if runtime.GOOS != "windows" {
require.NoError(t, err)
}
os.Remove("./out")
}
func (s *PluginSuite) InitBroker(procCfg *csconfig.PluginCfg) (*PluginBroker, error) {
pb := PluginBroker{}
if procCfg == nil {
procCfg = &csconfig.PluginCfg{}
}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
err := pb.Init(procCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: s.pluginDir,
NotificationDir: s.notifDir,
})
s.pluginBroker = &pb
return s.pluginBroker, err
}

View file

@ -5,11 +5,6 @@ package csplugin
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"testing" "testing"
"time" "time"
@ -25,116 +20,39 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
) )
var testPath string
func setPluginPermTo744(t *testing.T) { func (s *PluginSuite) permissionSetter(perm os.FileMode) func(*testing.T) {
setPluginPermTo(t, "744") return func(t *testing.T) {
} err := os.Chmod(s.pluginBinary, perm)
require.NoError(t, err, "chmod %s %s", perm, s.pluginBinary)
func setPluginPermTo722(t *testing.T) {
setPluginPermTo(t, "722")
}
func setPluginPermTo724(t *testing.T) {
setPluginPermTo(t, "724")
}
func TestGetPluginNameAndTypeFromPath(t *testing.T) {
setUp(t)
defer tearDown(t)
type args struct {
path string
}
tests := []struct {
name string
args args
want string
want1 string
expectedErr string
}{
{
name: "valid plugin name, single dash",
args: args{
path: path.Join(testPath, "notification-gitter"),
},
want: "notification",
want1: "gitter",
},
{
name: "invalid plugin name",
args: args{
path: "./tests/gitter",
},
expectedErr: "plugin name ./tests/gitter is invalid. Name should be like {type-name}",
},
{
name: "valid plugin name, multiple dash",
args: args{
path: "./tests/notification-instant-slack",
},
want: "notification-instant",
want1: "slack",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, got1, err := getPluginTypeAndSubtypeFromPath(tc.args.path)
cstest.RequireErrorContains(t, err, tc.expectedErr)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.want1, got1)
})
} }
} }
func TestListFilesAtPath(t *testing.T) { func (s *PluginSuite) readconfig() (PluginConfig) {
setUp(t) var config PluginConfig
defer tearDown(t) t := s.T()
type args struct {
path string
}
tests := []struct {
name string
args args
want []string
expectedErr string
}{
{
name: "valid directory",
args: args{
path: testPath,
},
want: []string{
filepath.Join(testPath, "notification-gitter"),
filepath.Join(testPath, "slack"),
},
},
{
name: "invalid directory",
args: args{
path: "./foo/bar/",
},
expectedErr: "open ./foo/bar/: " + cstest.FileNotFoundMessage,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, err := listFilesAtPath(tc.args.path)
cstest.RequireErrorContains(t, err, tc.expectedErr)
if !reflect.DeepEqual(got, tc.want) { orig, err := os.ReadFile(s.pluginConfig)
t.Errorf("listFilesAtPath() = %v, want %v", got, tc.want) require.NoError(t, err,"unable to read config file %s", s.pluginConfig)
}
}) err = yaml.Unmarshal(orig, &config)
} require.NoError(t, err,"unable to unmarshal config file")
return config
} }
func TestBrokerInit(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping test on windows")
}
func (s *PluginSuite) writeconfig(config PluginConfig) {
t := s.T()
data, err := yaml.Marshal(&config)
require.NoError(t, err,"unable to marshal config file")
err = os.WriteFile(s.pluginConfig, data, 0644)
require.NoError(t, err,"unable to write config file %s", s.pluginConfig)
}
func (s *PluginSuite) TestBrokerInit() {
tests := []struct { tests := []struct {
name string name string
action func(*testing.T) action func(*testing.T)
@ -143,28 +61,30 @@ func TestBrokerInit(t *testing.T) {
}{ }{
{ {
name: "valid config", name: "valid config",
action: setPluginPermTo744,
}, },
{ {
name: "group writable binary", name: "group writable binary",
expectedErr: "notification-dummy is world writable", expectedErr: "notification-dummy is world writable",
action: setPluginPermTo722, action: s.permissionSetter(0o722),
}, },
{ {
name: "group writable binary", name: "group writable binary",
expectedErr: "notification-dummy is group writable", expectedErr: "notification-dummy is group writable",
action: setPluginPermTo724, action: s.permissionSetter(0o724),
}, },
{ {
name: "no plugin dir", name: "no plugin dir",
expectedErr: cstest.FileNotFoundMessage, expectedErr: cstest.FileNotFoundMessage,
action: tearDown, action: func(t *testing.T) {
err := os.RemoveAll(s.runDir)
require.NoError(t, err)
},
}, },
{ {
name: "no plugin binary", name: "no plugin binary",
expectedErr: "binary for plugin dummy_default not found", expectedErr: "binary for plugin dummy_default not found",
action: func(t *testing.T) { action: func(t *testing.T) {
err := os.Remove(path.Join(testPath, "notification-dummy")) err := os.Remove(s.pluginBinary)
require.NoError(t, err) require.NoError(t, err)
}, },
}, },
@ -174,7 +94,6 @@ func TestBrokerInit(t *testing.T) {
procCfg: csconfig.PluginCfg{ procCfg: csconfig.PluginCfg{
User: "123445555551122toto", User: "123445555551122toto",
}, },
action: setPluginPermTo744,
}, },
{ {
name: "only specify group", name: "only specify group",
@ -182,7 +101,6 @@ func TestBrokerInit(t *testing.T) {
procCfg: csconfig.PluginCfg{ procCfg: csconfig.PluginCfg{
Group: "123445555551122toto", Group: "123445555551122toto",
}, },
action: setPluginPermTo744,
}, },
{ {
name: "Fails to run as root", name: "Fails to run as root",
@ -191,7 +109,6 @@ func TestBrokerInit(t *testing.T) {
User: "root", User: "root",
Group: "root", Group: "root",
}, },
action: setPluginPermTo744,
}, },
{ {
name: "Invalid user and group", name: "Invalid user and group",
@ -200,7 +117,6 @@ func TestBrokerInit(t *testing.T) {
User: "toto1234", User: "toto1234",
Group: "toto1234", Group: "toto1234",
}, },
action: setPluginPermTo744,
}, },
{ {
name: "Valid user and invalid group", name: "Valid user and invalid group",
@ -209,79 +125,33 @@ func TestBrokerInit(t *testing.T) {
User: "nobody", User: "nobody",
Group: "toto1234", Group: "toto1234",
}, },
action: setPluginPermTo744,
}, },
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { s.Run(tc.name, func() {
defer tearDown(t) t := s.T()
buildDummyPlugin(t)
if tc.action != nil { if tc.action != nil {
tc.action(t) tc.action(t)
} }
pb := PluginBroker{} _, err := s.InitBroker(&tc.procCfg)
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
err := pb.Init(&tc.procCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
defer pb.Kill()
cstest.RequireErrorContains(t, err, tc.expectedErr) cstest.RequireErrorContains(t, err, tc.expectedErr)
}) })
} }
} }
func readconfig(t *testing.T, path string) ([]byte, PluginConfig) { func (s *PluginSuite) TestBrokerNoThreshold() {
var config PluginConfig
orig, err := os.ReadFile("tests/notifications/dummy.yaml")
require.NoError(t, err,"unable to read config file %s", path)
err = yaml.Unmarshal(orig, &config)
require.NoError(t, err,"unable to unmarshal config file")
return orig, config
}
func writeconfig(t *testing.T, config PluginConfig, path string) {
data, err := yaml.Marshal(&config)
require.NoError(t, err,"unable to marshal config file")
err = os.WriteFile(path, data, 0644)
require.NoError(t, err,"unable to write config file %s", path)
}
func TestBrokerNoThreshold(t *testing.T) {
var alerts []models.Alert var alerts []models.Alert
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t) t := s.T()
setPluginPermTo744(t)
defer tearDown(t)
// 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",
})
pb, err := s.InitBroker(nil)
assert.NoError(t, err) assert.NoError(t, err)
tomb := tomb.Tomb{}
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
// send one item, it should be processed right now // send one item, it should be processed right now
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@ -292,7 +162,7 @@ func TestBrokerNoThreshold(t *testing.T) {
require.NoError(t, err, "Error reading file") require.NoError(t, err, "Error reading file")
err = json.Unmarshal(content, &alerts) err = json.Unmarshal(content, &alerts)
assert.NoError(t, err) require.NoError(t, err)
assert.Len(t, alerts, 1) assert.Len(t, alerts, 1)
// remove it // remove it
@ -313,34 +183,24 @@ func TestBrokerNoThreshold(t *testing.T) {
assert.Len(t, alerts, 1) assert.Len(t, alerts, 1)
} }
func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) { func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_TimeFirst() {
// test grouping by "time" // test grouping by "time"
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t)
setPluginPermTo744(t)
defer tearDown(t)
// init t := s.T()
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 // set groupwait and groupthreshold, should honor whichever comes first
raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") cfg := s.readconfig()
cfg.GroupThreshold = 4 cfg.GroupThreshold = 4
cfg.GroupWait = 1 * time.Second cfg.GroupWait = 1 * time.Second
writeconfig(t, cfg, "tests/notifications/dummy.yaml") s.writeconfig(cfg)
err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
assert.NoError(t, err)
tomb := tomb.Tomb{}
pb, err := s.InitBroker(nil)
assert.NoError(t, err)
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
// send data // 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{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@ -357,40 +217,24 @@ func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) {
err = json.Unmarshal(content, &alerts) err = json.Unmarshal(content, &alerts)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, alerts, 3) assert.Len(t, alerts, 3)
// restore config
err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
require.NoError(t, err,"unable to write config file")
} }
func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) { func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_CountFirst() {
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t)
setPluginPermTo(t, "744")
defer tearDown(t)
// init t := s.T()
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 // set groupwait and groupthreshold, should honor whichever comes first
raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") cfg := s.readconfig()
cfg.GroupThreshold = 4 cfg.GroupThreshold = 4
cfg.GroupWait = 4 * time.Second cfg.GroupWait = 4 * time.Second
writeconfig(t, cfg, "tests/notifications/dummy.yaml") s.writeconfig(cfg)
err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
assert.NoError(t, err)
tomb := tomb.Tomb{}
pb, err := s.InitBroker(nil)
assert.NoError(t, err)
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
// send data // send data
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@ -411,41 +255,24 @@ func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) {
err = json.Unmarshal(content, &alerts) err = json.Unmarshal(content, &alerts)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, alerts, 4) assert.Len(t, alerts, 4)
// restore config
err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
require.NoError(t, err,"unable to write config file")
} }
func TestBrokerRunGroupThreshold(t *testing.T) { func (s *PluginSuite) TestBrokerRunGroupThreshold() {
// test grouping by "size" // test grouping by "size"
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t)
setPluginPermTo(t, "744")
defer tearDown(t)
// init t := s.T()
pluginCfg := csconfig.PluginCfg{}
pb := PluginBroker{}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
// set groupwait // set groupwait
raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") cfg := s.readconfig()
cfg.GroupThreshold = 4 cfg.GroupThreshold = 4
writeconfig(t, cfg, "tests/notifications/dummy.yaml") s.writeconfig(cfg)
err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
pb, err := s.InitBroker(nil)
assert.NoError(t, err) assert.NoError(t, err)
tomb := tomb.Tomb{}
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
// send data // send data
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@ -466,39 +293,23 @@ func TestBrokerRunGroupThreshold(t *testing.T) {
err = json.Unmarshal(content, &alerts) err = json.Unmarshal(content, &alerts)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, alerts, 4) assert.Len(t, alerts, 4)
// restore config
err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
require.NoError(t, err, "unable to write config file")
} }
func TestBrokerRunTimeThreshold(t *testing.T) { func (s *PluginSuite) TestBrokerRunTimeThreshold() {
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t)
setPluginPermTo(t, "744")
defer tearDown(t)
// init t := s.T()
pluginCfg := csconfig.PluginCfg{}
pb := PluginBroker{}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
// set groupwait // set groupwait
raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") cfg := s.readconfig()
cfg.GroupWait = 1 * time.Second cfg.GroupWait = 1 * time.Second
writeconfig(t, cfg, "tests/notifications/dummy.yaml") s.writeconfig(cfg)
err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
assert.NoError(t, err)
tomb := tomb.Tomb{}
pb, err := s.InitBroker(nil)
assert.NoError(t, err)
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
// send data // send data
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@ -516,32 +327,18 @@ func TestBrokerRunTimeThreshold(t *testing.T) {
err = json.Unmarshal(content, &alerts) err = json.Unmarshal(content, &alerts)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, alerts, 1) assert.Len(t, alerts, 1)
// restore config
err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644)
require.NoError(t, err, "unable to write config file %s", err)
} }
func TestBrokerRunSimple(t *testing.T) { func (s *PluginSuite) TestBrokerRunSimple() {
DefaultEmptyTicker = 50 * time.Millisecond DefaultEmptyTicker = 50 * time.Millisecond
buildDummyPlugin(t)
setPluginPermTo(t, "744")
defer tearDown(t)
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{}
t := s.T()
pb, err := s.InitBroker(nil)
assert.NoError(t, err)
tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
assert.NoFileExists(t, "./out") assert.NoFileExists(t, "./out")
@ -559,47 +356,3 @@ func TestBrokerRunSimple(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, alerts, 2) assert.Len(t, alerts, 2)
} }
func buildDummyPlugin(t *testing.T) {
dir, err := os.MkdirTemp("./tests", "cs_plugin_test")
require.NoError(t, err)
cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/")
err = cmd.Run()
require.NoError(t, err, "while building dummy plugin")
testPath = dir
os.Remove("./out")
}
func setPluginPermTo(t *testing.T, perm string) {
if runtime.GOOS != "windows" {
err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run()
require.NoError(t, err, "chmod 744 %s", path.Join(testPath, "notification-dummy"))
}
}
func setUp(t *testing.T) {
dir, err := os.MkdirTemp("./", "cs_plugin_test")
require.NoError(t, err)
f, err := os.Create(path.Join(dir, "slack"))
require.NoError(t, err)
f.Close()
f, err = os.Create(path.Join(dir, "notification-gitter"))
require.NoError(t, err)
f.Close()
err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666)
require.NoError(t, err)
testPath = dir
}
func tearDown(t *testing.T) {
err := os.RemoveAll(testPath)
require.NoError(t, err)
os.Remove("./out")
}

View file

@ -3,16 +3,12 @@
package csplugin package csplugin
import ( import (
"log"
"os" "os"
"os/exec"
"path"
"path/filepath"
"reflect"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/tomb.v2" "gopkg.in/tomb.v2"
"github.com/crowdsecurity/go-cs-lib/pkg/cstest" "github.com/crowdsecurity/go-cs-lib/pkg/cstest"
@ -27,188 +23,55 @@ Due to the complexity of file permission modification with go on windows, we onl
not if it will actually reject plugins with invalid permissions not if it will actually reject plugins with invalid permissions
*/ */
var testPath string func (s *PluginSuite) TestBrokerInit() {
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.exe",
},
want: "",
want1: "",
wantErr: true,
},
{
name: "valid plugin name, multiple dash",
args: args{
path: ".\\tests\\notification-instant-slack.exe",
},
want: "notification-instant",
want1: "slack",
wantErr: false,
},
}
for _, tt := range tests {
tt := tt
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{
filepath.Join(testPath, "notification-gitter"),
filepath.Join(testPath, "slack"),
},
},
{
name: "invalid directory",
args: args{
path: "./foo/bar/",
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
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 { tests := []struct {
name string name string
action func() action func(*testing.T)
errContains string
wantErr bool
procCfg csconfig.PluginCfg procCfg csconfig.PluginCfg
expectedErr string
}{ }{
{ {
name: "valid config", name: "valid config",
wantErr: false,
}, },
{ {
name: "no plugin dir", name: "no plugin dir",
wantErr: true, expectedErr: cstest.PathNotFoundMessage,
errContains: cstest.FileNotFoundMessage, action: func(t *testing.T) {
action: tearDown, err := os.RemoveAll(s.runDir)
require.NoError(t, err)
},
}, },
{ {
name: "no plugin binary", name: "no plugin binary",
wantErr: true, expectedErr: "binary for plugin dummy_default not found",
errContains: "binary for plugin dummy_default not found", action: func(t *testing.T) {
action: func() { err := os.Remove(s.pluginBinary)
err := os.Remove(path.Join(testPath, "notification-dummy.exe")) require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
}, },
}, },
} }
for _, test := range tests { for _, tc := range tests {
test := test tc := tc
t.Run(test.name, func(t *testing.T) { s.Run(tc.name, func() {
defer tearDown() t := s.T()
buildDummyPlugin() if tc.action != nil {
if test.action != nil { tc.action(t)
test.action()
} }
pb := PluginBroker{} _, err := s.InitBroker(&tc.procCfg)
profiles := csconfig.NewDefaultConfig().API.Server.Profiles cstest.RequireErrorContains(t, err, tc.expectedErr)
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 TestBrokerRun(t *testing.T) { func (s *PluginSuite) TestBrokerRun() {
buildDummyPlugin() t := s.T()
defer tearDown()
procCfg := csconfig.PluginCfg{} pb, err := s.InitBroker(nil)
pb := PluginBroker{}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
err := pb.Init(&procCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
assert.NoError(t, err) assert.NoError(t, err)
tomb := tomb.Tomb{} tomb := tomb.Tomb{}
go pb.Run(&tomb) go pb.Run(&tomb)
defer pb.Kill()
assert.NoFileExists(t, "./out") assert.NoFileExists(t, "./out")
defer os.Remove("./out") defer os.Remove("./out")
@ -220,44 +83,3 @@ func TestBrokerRun(t *testing.T) {
assert.FileExists(t, ".\\out") assert.FileExists(t, ".\\out")
assert.Equal(t, types.GetLineCountForFile(".\\out"), 2) assert.Equal(t, types.GetLineCountForFile(".\\out"), 2)
} }
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.exe"), "../../plugins/notifications/dummy/")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
testPath = dir
}
func setUp() {
dir, err := os.MkdirTemp("./", "cs_plugin_test")
if err != nil {
log.Fatal(err)
}
f, err := os.Create(path.Join(dir, "slack"))
if err != nil {
log.Fatal(err)
}
f.Close()
f, err = os.Create(path.Join(dir, "notification-gitter"))
if err != nil {
log.Fatal(err)
}
f.Close()
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)
}
}

22
pkg/csplugin/listfiles.go Normal file
View file

@ -0,0 +1,22 @@
package csplugin
import (
"os"
"path/filepath"
)
// helper which gives paths to all files in the given directory non-recursively
func listFilesAtPath(path string) ([]string, error) {
filePaths := make([]string, 0)
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
for _, file := range files {
if ! file.IsDir() {
filePaths = append(filePaths, filepath.Join(path, file.Name()))
}
}
return filePaths, nil
}

View file

@ -0,0 +1,57 @@
package csplugin
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/go-cs-lib/pkg/cstest"
)
func TestListFilesAtPath(t *testing.T) {
dir, err := os.MkdirTemp("", "test-listfiles")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(dir)
})
_, err = os.Create(filepath.Join(dir, "notification-gitter"))
require.NoError(t, err)
_, err = os.Create(filepath.Join(dir, "slack"))
require.NoError(t, err)
err = os.Mkdir(filepath.Join(dir, "somedir"), 0755)
require.NoError(t, err)
_, err = os.Create(filepath.Join(dir, "somedir", "inner"))
require.NoError(t, err)
tests := []struct {
name string
path string
want []string
expectedErr string
}{
{
name: "valid directory",
path: dir,
want: []string{
filepath.Join(dir, "notification-gitter"),
filepath.Join(dir, "slack"),
},
},
{
name: "invalid directory",
path: "./foo/bar/",
expectedErr: "open ./foo/bar/: " + cstest.PathNotFoundMessage,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, err := listFilesAtPath(tc.path)
cstest.RequireErrorContains(t, err, tc.expectedErr)
assert.ElementsMatch(t, tc.want, got)
})
}
}

View file

@ -0,0 +1,49 @@
//go:build linux || freebsd || netbsd || openbsd || solaris || !windows
package csplugin
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/crowdsecurity/go-cs-lib/pkg/cstest"
)
func TestGetPluginNameAndTypeFromPath(t *testing.T) {
tests := []struct {
name string
path string
want string
want1 string
expectedErr string
}{
{
name: "valid plugin name, single dash",
path: "/path/to/notification-gitter",
want: "notification",
want1: "gitter",
},
{
name: "invalid plugin name",
path: "/path/to/gitter",
expectedErr: "plugin name /path/to/gitter is invalid. Name should be like {type-name}",
},
{
name: "valid plugin name, multiple dash",
path: "/path/to/notification-instant-slack",
want: "notification-instant",
want1: "slack",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, got1, err := getPluginTypeAndSubtypeFromPath(tc.path)
cstest.RequireErrorContains(t, err, tc.expectedErr)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.want1, got1)
})
}
}

View file

@ -0,0 +1,49 @@
//go:build windows
package csplugin
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/crowdsecurity/go-cs-lib/pkg/cstest"
)
func TestGetPluginNameAndTypeFromPath(t *testing.T) {
tests := []struct {
name string
path string
want string
want1 string
expectedErr string
}{
{
name: "valid plugin name, single dash",
path: "c:\\path\\to\\notification-gitter",
want: "notification",
want1: "gitter",
},
{
name: "invalid plugin name",
path: "c:\\path\\to\\gitter.exe",
expectedErr: "plugin name c:\\path\\to\\gitter.exe is invalid. Name should be like {type-name}",
},
{
name: "valid plugin name, multiple dash",
path: "c:\\path\\to\\notification-instant-slack.exe",
want: "notification-instant",
want1: "slack",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got, got1, err := getPluginTypeAndSubtypeFromPath(tc.path)
cstest.RequireErrorContains(t, err, tc.expectedErr)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.want1, got1)
})
}
}