* Fix races in test (#1446) Co-authored-by: Shivam Sandbhor <shivam.sandbhor@gmail.com> Co-authored-by: alteredCoder <kevin@crowdsec.net> Co-authored-by: AlteredCoder <64792091+AlteredCoder@users.noreply.github.com>
This commit is contained in:
parent
0f7f21a7db
commit
e6a35e8714
|
@ -37,6 +37,9 @@ const (
|
||||||
CrowdsecPluginKey string = "CROWDSEC_PLUGIN_KEY"
|
CrowdsecPluginKey string = "CROWDSEC_PLUGIN_KEY"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//The broker is reponsible for running the plugins and dispatching events
|
||||||
|
//It receives all the events from the main process and stacks them up
|
||||||
|
//It is as well notified by the watcher when it needs to deliver events to plugins (based on time or count threshold)
|
||||||
type PluginBroker struct {
|
type PluginBroker struct {
|
||||||
PluginChannel chan ProfileAlert
|
PluginChannel chan ProfileAlert
|
||||||
alertsByPluginName map[string][]*models.Alert
|
alertsByPluginName map[string][]*models.Alert
|
||||||
|
@ -55,12 +58,12 @@ type PluginBroker struct {
|
||||||
type PluginConfig struct {
|
type PluginConfig struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
GroupWait time.Duration `yaml:"group_wait"`
|
GroupWait time.Duration `yaml:"group_wait,omitempty"`
|
||||||
GroupThreshold int `yaml:"group_threshold"`
|
GroupThreshold int `yaml:"group_threshold,omitempty"`
|
||||||
MaxRetry int `yaml:"max_retry"`
|
MaxRetry int `yaml:"max_retry,omitempty"`
|
||||||
TimeOut time.Duration `yaml:"timeout"`
|
TimeOut time.Duration `yaml:"timeout,omitempty"`
|
||||||
|
|
||||||
Format string `yaml:"format"` // specific to notification plugins
|
Format string `yaml:"format,omitempty"` // specific to notification plugins
|
||||||
|
|
||||||
Config map[string]interface{} `yaml:",inline"` //to keep the plugin-specific config
|
Config map[string]interface{} `yaml:",inline"` //to keep the plugin-specific config
|
||||||
|
|
||||||
|
@ -100,6 +103,7 @@ func (pb *PluginBroker) Kill() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb *PluginBroker) Run(tomb *tomb.Tomb) {
|
func (pb *PluginBroker) Run(tomb *tomb.Tomb) {
|
||||||
|
//we get signaled via the channel when notifications need to be delivered to plugin (via the watcher)
|
||||||
pb.watcher.Start(tomb)
|
pb.watcher.Start(tomb)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -109,6 +113,7 @@ func (pb *PluginBroker) Run(tomb *tomb.Tomb) {
|
||||||
case pluginName := <-pb.watcher.PluginEvents:
|
case pluginName := <-pb.watcher.PluginEvents:
|
||||||
// this can be ran in goroutine, but then locks will be needed
|
// this can be ran in goroutine, but then locks will be needed
|
||||||
pluginMutex.Lock()
|
pluginMutex.Lock()
|
||||||
|
log.Tracef("going to deliver %d alerts to plugin %s", len(pb.alertsByPluginName[pluginName]), pluginName)
|
||||||
tmpAlerts := pb.alertsByPluginName[pluginName]
|
tmpAlerts := pb.alertsByPluginName[pluginName]
|
||||||
pb.alertsByPluginName[pluginName] = make([]*models.Alert, 0)
|
pb.alertsByPluginName[pluginName] = make([]*models.Alert, 0)
|
||||||
pluginMutex.Unlock()
|
pluginMutex.Unlock()
|
||||||
|
@ -248,10 +253,12 @@ func (pb *PluginBroker) loadPlugins(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (Notifier, error) {
|
func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (Notifier, error) {
|
||||||
|
|
||||||
handshake, err := getHandshake()
|
handshake, err := getHandshake()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Debugf("Executing plugin %s", binaryPath)
|
||||||
cmd := exec.Command(binaryPath)
|
cmd := exec.Command(binaryPath)
|
||||||
if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" {
|
if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" {
|
||||||
if !(pb.pluginProcConfig.User != "" && pb.pluginProcConfig.Group != "") {
|
if !(pb.pluginProcConfig.User != "" && pb.pluginProcConfig.Group != "") {
|
||||||
|
@ -293,7 +300,7 @@ func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb *PluginBroker) pushNotificationsToPlugin(pluginName string, alerts []*models.Alert) error {
|
func (pb *PluginBroker) pushNotificationsToPlugin(pluginName string, alerts []*models.Alert) error {
|
||||||
log.WithField("plugin", pluginName).Debug("pushing alerts to plugin")
|
log.WithField("plugin", pluginName).Debugf("pushing %d alerts to plugin", len(alerts))
|
||||||
if len(alerts) == 0 {
|
if len(alerts) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -356,9 +363,6 @@ func setRequiredFields(pluginCfg *PluginConfig) {
|
||||||
pluginCfg.TimeOut = time.Second * 5
|
pluginCfg.TimeOut = time.Second * 5
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginCfg.GroupWait == time.Second*0 {
|
|
||||||
pluginCfg.GroupWait = time.Second * 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pluginIsValid(path string) error {
|
func pluginIsValid(path string) error {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package csplugin
|
package csplugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
@ -9,12 +10,14 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gopkg.in/tomb.v2"
|
"gopkg.in/tomb.v2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testPath string
|
var testPath string
|
||||||
|
@ -252,17 +255,282 @@ func TestBrokerInit(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBrokerRun(t *testing.T) {
|
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()
|
buildDummyPlugin()
|
||||||
setPluginPermTo744()
|
setPluginPermTo744()
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
procCfg := csconfig.PluginCfg{}
|
//init
|
||||||
|
pluginCfg := csconfig.PluginCfg{}
|
||||||
pb := PluginBroker{}
|
pb := PluginBroker{}
|
||||||
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
|
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
|
||||||
profiles = append(profiles, &csconfig.ProfileCfg{
|
profiles = append(profiles, &csconfig.ProfileCfg{
|
||||||
Notifications: []string{"dummy_default"},
|
Notifications: []string{"dummy_default"},
|
||||||
})
|
})
|
||||||
err := pb.Init(&procCfg, profiles, &csconfig.ConfigurationPaths{
|
//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,
|
PluginDir: testPath,
|
||||||
NotificationDir: "./tests/notifications",
|
NotificationDir: "./tests/notifications",
|
||||||
})
|
})
|
||||||
|
@ -276,10 +544,16 @@ func TestBrokerRun(t *testing.T) {
|
||||||
|
|
||||||
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{}}
|
||||||
time.Sleep(time.Second * 4)
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
|
||||||
assert.FileExists(t, "./out")
|
content, err := ioutil.ReadFile("./out")
|
||||||
assert.Equal(t, types.GetLineCountForFile("./out"), 2)
|
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() {
|
func buildDummyPlugin() {
|
||||||
|
@ -292,6 +566,7 @@ func buildDummyPlugin() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
testPath = dir
|
testPath = dir
|
||||||
|
os.Remove("./out")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPluginPermTo(perm string) {
|
func setPluginPermTo(perm string) {
|
||||||
|
@ -325,4 +600,5 @@ func tearDown() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
os.Remove("./out")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,66 @@
|
||||||
package csplugin
|
package csplugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/tomb.v2"
|
"gopkg.in/tomb.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
PluginWatcher is here to allow grouping and threshold features for notification plugins :
|
||||||
|
by frequency : it will signal the plugin to deliver notifications at this frequence (watchPluginTicker)
|
||||||
|
by threshold : it will signal the plugin to deliver notifications when the number of alerts for this plugin reaches this threshold (watchPluginAlertCounts)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: When we start using go 1.18, consider moving this struct in some utils pkg. Make the implementation more generic using generics :)
|
||||||
|
type alertCounterByPluginName struct {
|
||||||
|
sync.Mutex
|
||||||
|
data map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAlertCounterByPluginName() alertCounterByPluginName {
|
||||||
|
return alertCounterByPluginName{
|
||||||
|
data: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acp *alertCounterByPluginName) Init() {
|
||||||
|
acp.data = make(map[string]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acp *alertCounterByPluginName) Get(key string) (int, bool) {
|
||||||
|
acp.Lock()
|
||||||
|
val, ok := acp.data[key]
|
||||||
|
acp.Unlock()
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (acp *alertCounterByPluginName) Set(key string, val int) {
|
||||||
|
acp.Lock()
|
||||||
|
acp.data[key] = val
|
||||||
|
acp.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
type PluginWatcher struct {
|
type PluginWatcher struct {
|
||||||
PluginConfigByName map[string]PluginConfig
|
PluginConfigByName map[string]PluginConfig
|
||||||
AlertCountByPluginName map[string]int
|
AlertCountByPluginName alertCounterByPluginName
|
||||||
PluginEvents chan string
|
PluginEvents chan string
|
||||||
Inserts chan string
|
Inserts chan string
|
||||||
tomb *tomb.Tomb
|
tomb *tomb.Tomb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var DefaultEmptyTicker = time.Second * 1
|
||||||
|
|
||||||
func (pw *PluginWatcher) Init(configs map[string]PluginConfig, alertsByPluginName map[string][]*models.Alert) {
|
func (pw *PluginWatcher) Init(configs map[string]PluginConfig, alertsByPluginName map[string][]*models.Alert) {
|
||||||
pw.PluginConfigByName = configs
|
pw.PluginConfigByName = configs
|
||||||
pw.PluginEvents = make(chan string)
|
pw.PluginEvents = make(chan string)
|
||||||
pw.AlertCountByPluginName = make(map[string]int)
|
pw.AlertCountByPluginName = newAlertCounterByPluginName()
|
||||||
pw.Inserts = make(chan string)
|
pw.Inserts = make(chan string)
|
||||||
for name := range alertsByPluginName {
|
for name := range alertsByPluginName {
|
||||||
pw.AlertCountByPluginName[name] = 0
|
pw.AlertCountByPluginName.Set(name, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,15 +81,62 @@ func (pw *PluginWatcher) Start(tomb *tomb.Tomb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pw *PluginWatcher) watchPluginTicker(pluginName string) {
|
func (pw *PluginWatcher) watchPluginTicker(pluginName string) {
|
||||||
if pw.PluginConfigByName[pluginName].GroupWait <= time.Second*0 {
|
var watchTime time.Duration
|
||||||
return
|
var watchCount int = -1
|
||||||
|
// Threshold can be set : by time, by count, or both
|
||||||
|
// if only time is set, honor it
|
||||||
|
// if only count is set, put timer to 1 second and just check size
|
||||||
|
// if both are set, set timer to 1 second, but check size && time
|
||||||
|
interval := pw.PluginConfigByName[pluginName].GroupWait
|
||||||
|
threshold := pw.PluginConfigByName[pluginName].GroupThreshold
|
||||||
|
|
||||||
|
//only size is set
|
||||||
|
if threshold > 0 && interval == 0 {
|
||||||
|
watchCount = threshold
|
||||||
|
watchTime = DefaultEmptyTicker
|
||||||
|
} else if interval != 0 && threshold == 0 {
|
||||||
|
//only time is set
|
||||||
|
watchTime = interval
|
||||||
|
} else if interval != 0 && threshold != 0 {
|
||||||
|
//both are set
|
||||||
|
watchTime = DefaultEmptyTicker
|
||||||
|
watchCount = threshold
|
||||||
|
} else {
|
||||||
|
//none are set, we sent every event we receive
|
||||||
|
watchTime = DefaultEmptyTicker
|
||||||
|
watchCount = 1
|
||||||
}
|
}
|
||||||
ticker := time.NewTicker(pw.PluginConfigByName[pluginName].GroupWait)
|
|
||||||
|
ticker := time.NewTicker(watchTime)
|
||||||
|
var lastSend time.Time = time.Now()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
pw.PluginEvents <- pluginName
|
send := false
|
||||||
|
//if count threshold was set, honor no matter what
|
||||||
|
if pc, _ := pw.AlertCountByPluginName.Get(pluginName); watchCount > 0 && pc >= watchCount {
|
||||||
|
log.Tracef("[%s] %d alerts received, sending\n", pluginName, pc)
|
||||||
|
send = true
|
||||||
|
pw.AlertCountByPluginName.Set(pluginName, 0)
|
||||||
|
}
|
||||||
|
//if time threshold only was set
|
||||||
|
if watchTime > 0 && watchTime == interval {
|
||||||
|
log.Tracef("sending alerts to %s, duration %s elapsed", pluginName, interval)
|
||||||
|
send = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//if we hit timer because it was set low to honor count, check if we should trigger
|
||||||
|
if watchTime == DefaultEmptyTicker && watchTime != interval && interval != 0 {
|
||||||
|
if lastSend.Add(interval).Before(time.Now()) {
|
||||||
|
log.Tracef("sending alerts to %s, duration %s elapsed", pluginName, interval)
|
||||||
|
send = true
|
||||||
|
lastSend = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if send {
|
||||||
|
log.Tracef("sending alerts to %s", pluginName)
|
||||||
|
pw.PluginEvents <- pluginName
|
||||||
|
}
|
||||||
case <-pw.tomb.Dying():
|
case <-pw.tomb.Dying():
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
return
|
return
|
||||||
|
@ -62,12 +148,10 @@ func (pw *PluginWatcher) watchPluginAlertCounts() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case pluginName := <-pw.Inserts:
|
case pluginName := <-pw.Inserts:
|
||||||
if threshold := pw.PluginConfigByName[pluginName].GroupThreshold; threshold > 0 {
|
//we only "count" pending alerts, and watchPluginTicker is actually going to send it
|
||||||
pw.AlertCountByPluginName[pluginName]++
|
if _, ok := pw.PluginConfigByName[pluginName]; ok {
|
||||||
if pw.AlertCountByPluginName[pluginName] >= threshold {
|
curr, _ := pw.AlertCountByPluginName.Get(pluginName)
|
||||||
pw.PluginEvents <- pluginName
|
pw.AlertCountByPluginName.Set(pluginName, curr+1)
|
||||||
pw.AlertCountByPluginName[pluginName] = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case <-pw.tomb.Dying():
|
case <-pw.tomb.Dying():
|
||||||
return
|
return
|
||||||
|
|
|
@ -21,9 +21,11 @@ func resetTestTomb(testTomb *tomb.Tomb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetWatcherAlertCounter(pw *PluginWatcher) {
|
func resetWatcherAlertCounter(pw *PluginWatcher) {
|
||||||
for k := range pw.AlertCountByPluginName {
|
pw.AlertCountByPluginName.Lock()
|
||||||
pw.AlertCountByPluginName[k] = 0
|
for k := range pw.AlertCountByPluginName.data {
|
||||||
|
pw.AlertCountByPluginName.data[k] = 0
|
||||||
}
|
}
|
||||||
|
pw.AlertCountByPluginName.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertNAlertsToPlugin(pw *PluginWatcher, n int, pluginName string) {
|
func insertNAlertsToPlugin(pw *PluginWatcher, n int, pluginName string) {
|
||||||
|
@ -34,7 +36,8 @@ func insertNAlertsToPlugin(pw *PluginWatcher, n int, pluginName string) {
|
||||||
|
|
||||||
func listenChannelWithTimeout(ctx context.Context, channel chan string) error {
|
func listenChannelWithTimeout(ctx context.Context, channel chan string) error {
|
||||||
select {
|
select {
|
||||||
case <-channel:
|
case x := <-channel:
|
||||||
|
log.Printf("received -> %v", x)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,7 +248,7 @@ func UtcNow() time.Time {
|
||||||
func GetLineCountForFile(filepath string) int {
|
func GetLineCountForFile(filepath string) int {
|
||||||
f, err := os.Open(filepath)
|
f, err := os.Open(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unable to open log file %s", filepath)
|
log.Fatalf("unable to open log file %s : %s", filepath, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
lc := 0
|
lc := 0
|
||||||
|
|
|
@ -54,6 +54,7 @@ setup() {
|
||||||
|
|
||||||
run -0 cscli decisions add --ip 1.2.3.5 --duration 30s
|
run -0 cscli decisions add --ip 1.2.3.5 --duration 30s
|
||||||
assert_output --partial 'Decision successfully added'
|
assert_output --partial 'Decision successfully added'
|
||||||
|
sleep 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "$FILE expected 1 log line from http server" {
|
@test "$FILE expected 1 log line from http server" {
|
||||||
|
|
|
@ -21,6 +21,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||||
self.send_header('Content-type','application/json')
|
self.send_header('Content-type','application/json')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps({}).encode())
|
self.wfile.write(json.dumps({}).encode())
|
||||||
|
self.wfile.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
|
|
Loading…
Reference in a new issue