github.com/crowdsecurity/crowdsec@v1.6.1/pkg/csplugin/broker_test.go (about)

     1  //go:build linux || freebsd || netbsd || openbsd || solaris || !windows
     2  
     3  package csplugin
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"io"
     9  	"os"
    10  	"testing"
    11  	"time"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	"gopkg.in/tomb.v2"
    17  	"gopkg.in/yaml.v2"
    18  
    19  	"github.com/crowdsecurity/go-cs-lib/cstest"
    20  
    21  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    22  	"github.com/crowdsecurity/crowdsec/pkg/models"
    23  )
    24  
    25  func (s *PluginSuite) permissionSetter(perm os.FileMode) func(*testing.T) {
    26  	return func(t *testing.T) {
    27  		err := os.Chmod(s.pluginBinary, perm)
    28  		require.NoError(t, err, "chmod %s %s", perm, s.pluginBinary)
    29  	}
    30  }
    31  
    32  func (s *PluginSuite) readconfig() PluginConfig {
    33  	var config PluginConfig
    34  
    35  	t := s.T()
    36  
    37  	orig, err := os.ReadFile(s.pluginConfig)
    38  	require.NoError(t, err, "unable to read config file %s", s.pluginConfig)
    39  
    40  	err = yaml.Unmarshal(orig, &config)
    41  	require.NoError(t, err, "unable to unmarshal config file")
    42  
    43  	return config
    44  }
    45  
    46  func (s *PluginSuite) writeconfig(config PluginConfig) {
    47  	t := s.T()
    48  	data, err := yaml.Marshal(&config)
    49  	require.NoError(t, err, "unable to marshal config file")
    50  
    51  	err = os.WriteFile(s.pluginConfig, data, 0644)
    52  	require.NoError(t, err, "unable to write config file %s", s.pluginConfig)
    53  }
    54  
    55  func (s *PluginSuite) TestBrokerInit() {
    56  	tests := []struct {
    57  		name        string
    58  		action      func(*testing.T)
    59  		procCfg     csconfig.PluginCfg
    60  		expectedErr string
    61  	}{
    62  		{
    63  			name: "valid config",
    64  		},
    65  		{
    66  			name:        "group writable binary",
    67  			expectedErr: "notification-dummy is world writable",
    68  			action:      s.permissionSetter(0o722),
    69  		},
    70  		{
    71  			name:        "group writable binary",
    72  			expectedErr: "notification-dummy is group writable",
    73  			action:      s.permissionSetter(0o724),
    74  		},
    75  		{
    76  			name:        "no plugin dir",
    77  			expectedErr: cstest.FileNotFoundMessage,
    78  			action: func(t *testing.T) {
    79  				err := os.RemoveAll(s.runDir)
    80  				require.NoError(t, err)
    81  			},
    82  		},
    83  		{
    84  			name:        "no plugin binary",
    85  			expectedErr: "binary for plugin dummy_default not found",
    86  			action: func(t *testing.T) {
    87  				err := os.Remove(s.pluginBinary)
    88  				require.NoError(t, err)
    89  			},
    90  		},
    91  		{
    92  			name:        "only specify user",
    93  			expectedErr: "both plugin user and group must be set",
    94  			procCfg: csconfig.PluginCfg{
    95  				User: "123445555551122toto",
    96  			},
    97  		},
    98  		{
    99  			name:        "only specify group",
   100  			expectedErr: "both plugin user and group must be set",
   101  			procCfg: csconfig.PluginCfg{
   102  				Group: "123445555551122toto",
   103  			},
   104  		},
   105  		{
   106  			name:        "Fails to run as root",
   107  			expectedErr: "operation not permitted",
   108  			procCfg: csconfig.PluginCfg{
   109  				User:  "root",
   110  				Group: "root",
   111  			},
   112  		},
   113  		{
   114  			name:        "Invalid user and group",
   115  			expectedErr: "toto1234",
   116  			procCfg: csconfig.PluginCfg{
   117  				User:  "toto1234",
   118  				Group: "toto1234",
   119  			},
   120  		},
   121  		{
   122  			name:        "Valid user and invalid group",
   123  			expectedErr: "toto1234",
   124  			procCfg: csconfig.PluginCfg{
   125  				User:  "nobody",
   126  				Group: "toto1234",
   127  			},
   128  		},
   129  	}
   130  
   131  	for _, tc := range tests {
   132  		tc := tc
   133  		s.Run(tc.name, func() {
   134  			t := s.T()
   135  			if tc.action != nil {
   136  				tc.action(t)
   137  			}
   138  			_, err := s.InitBroker(&tc.procCfg)
   139  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   140  		})
   141  	}
   142  }
   143  
   144  func (s *PluginSuite) TestBrokerNoThreshold() {
   145  	var alerts []models.Alert
   146  
   147  	DefaultEmptyTicker = 50 * time.Millisecond
   148  
   149  	t := s.T()
   150  
   151  	pb, err := s.InitBroker(nil)
   152  	require.NoError(t, err)
   153  
   154  	tomb := tomb.Tomb{}
   155  	go pb.Run(&tomb)
   156  
   157  	// send one item, it should be processed right now
   158  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   159  
   160  	time.Sleep(200 * time.Millisecond)
   161  
   162  	// we expect one now
   163  	content, err := os.ReadFile("./out")
   164  	require.NoError(t, err, "Error reading file")
   165  
   166  	err = json.Unmarshal(content, &alerts)
   167  	require.NoError(t, err)
   168  	assert.Len(t, alerts, 1)
   169  
   170  	// remove it
   171  	os.Remove("./out")
   172  
   173  	// and another one
   174  	log.Printf("second send")
   175  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   176  
   177  	time.Sleep(200 * time.Millisecond)
   178  
   179  	// we expect one again, as we cleaned the file
   180  	content, err = os.ReadFile("./out")
   181  	require.NoError(t, err, "Error reading file")
   182  
   183  	err = json.Unmarshal(content, &alerts)
   184  	log.Printf("content-> %s", content)
   185  	require.NoError(t, err)
   186  	assert.Len(t, alerts, 1)
   187  }
   188  
   189  func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_TimeFirst() {
   190  	// test grouping by "time"
   191  	DefaultEmptyTicker = 50 * time.Millisecond
   192  
   193  	t := s.T()
   194  
   195  	// set groupwait and groupthreshold, should honor whichever comes first
   196  	cfg := s.readconfig()
   197  	cfg.GroupThreshold = 4
   198  	cfg.GroupWait = 1 * time.Second
   199  	s.writeconfig(cfg)
   200  
   201  	pb, err := s.InitBroker(nil)
   202  	require.NoError(t, err)
   203  
   204  	tomb := tomb.Tomb{}
   205  	go pb.Run(&tomb)
   206  
   207  	// send data
   208  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   209  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   210  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   211  
   212  	time.Sleep(500 * time.Millisecond)
   213  	// because of group threshold, we shouldn't have data yet
   214  	assert.NoFileExists(t, "./out")
   215  	time.Sleep(1 * time.Second)
   216  	// after 1 seconds, we should have data
   217  	content, err := os.ReadFile("./out")
   218  	require.NoError(t, err)
   219  
   220  	var alerts []models.Alert
   221  	err = json.Unmarshal(content, &alerts)
   222  	require.NoError(t, err)
   223  	assert.Len(t, alerts, 3)
   224  }
   225  
   226  func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_CountFirst() {
   227  	DefaultEmptyTicker = 50 * time.Millisecond
   228  
   229  	t := s.T()
   230  
   231  	// set groupwait and groupthreshold, should honor whichever comes first
   232  	cfg := s.readconfig()
   233  	cfg.GroupThreshold = 4
   234  	cfg.GroupWait = 4 * time.Second
   235  	s.writeconfig(cfg)
   236  
   237  	pb, err := s.InitBroker(nil)
   238  	require.NoError(t, err)
   239  
   240  	tomb := tomb.Tomb{}
   241  	go pb.Run(&tomb)
   242  
   243  	// send data
   244  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   245  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   246  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   247  
   248  	time.Sleep(100 * time.Millisecond)
   249  
   250  	// because of group threshold, we shouldn't have data yet
   251  	assert.NoFileExists(t, "./out")
   252  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   253  
   254  	time.Sleep(100 * time.Millisecond)
   255  
   256  	// and now we should
   257  	content, err := os.ReadFile("./out")
   258  	require.NoError(t, err, "Error reading file")
   259  
   260  	var alerts []models.Alert
   261  	err = json.Unmarshal(content, &alerts)
   262  	require.NoError(t, err)
   263  	assert.Len(t, alerts, 4)
   264  }
   265  
   266  func (s *PluginSuite) TestBrokerRunGroupThreshold() {
   267  	// test grouping by "size"
   268  	DefaultEmptyTicker = 50 * time.Millisecond
   269  
   270  	t := s.T()
   271  
   272  	// set groupwait
   273  	cfg := s.readconfig()
   274  	cfg.GroupThreshold = 4
   275  	s.writeconfig(cfg)
   276  
   277  	pb, err := s.InitBroker(nil)
   278  	require.NoError(t, err)
   279  
   280  	tomb := tomb.Tomb{}
   281  	go pb.Run(&tomb)
   282  
   283  	// send data
   284  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   285  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   286  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   287  
   288  	time.Sleep(time.Second)
   289  
   290  	// because of group threshold, we shouldn't have data yet
   291  	assert.NoFileExists(t, "./out")
   292  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   293  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   294  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   295  
   296  	time.Sleep(time.Second)
   297  
   298  	// and now we should
   299  	content, err := os.ReadFile("./out")
   300  	require.NoError(t, err, "Error reading file")
   301  
   302  	decoder := json.NewDecoder(bytes.NewReader(content))
   303  
   304  	var alerts []models.Alert
   305  
   306  	// two notifications, one with 4 alerts, one with 2 alerts
   307  
   308  	err = decoder.Decode(&alerts)
   309  	require.NoError(t, err)
   310  	assert.Len(t, alerts, 4)
   311  
   312  	err = decoder.Decode(&alerts)
   313  	require.NoError(t, err)
   314  	assert.Len(t, alerts, 2)
   315  
   316  	err = decoder.Decode(&alerts)
   317  	assert.Equal(t, err, io.EOF)
   318  }
   319  
   320  func (s *PluginSuite) TestBrokerRunTimeThreshold() {
   321  	DefaultEmptyTicker = 50 * time.Millisecond
   322  
   323  	t := s.T()
   324  
   325  	// set groupwait
   326  	cfg := s.readconfig()
   327  	cfg.GroupWait = 1 * time.Second
   328  	s.writeconfig(cfg)
   329  
   330  	pb, err := s.InitBroker(nil)
   331  	require.NoError(t, err)
   332  
   333  	tomb := tomb.Tomb{}
   334  	go pb.Run(&tomb)
   335  
   336  	// send data
   337  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   338  
   339  	time.Sleep(200 * time.Millisecond)
   340  
   341  	// we shouldn't have data yet
   342  	assert.NoFileExists(t, "./out")
   343  	time.Sleep(1 * time.Second)
   344  
   345  	// and now we should
   346  	content, err := os.ReadFile("./out")
   347  	require.NoError(t, err, "Error reading file")
   348  
   349  	var alerts []models.Alert
   350  	err = json.Unmarshal(content, &alerts)
   351  	require.NoError(t, err)
   352  	assert.Len(t, alerts, 1)
   353  }
   354  
   355  func (s *PluginSuite) TestBrokerRunSimple() {
   356  	DefaultEmptyTicker = 50 * time.Millisecond
   357  
   358  	t := s.T()
   359  
   360  	pb, err := s.InitBroker(nil)
   361  	require.NoError(t, err)
   362  
   363  	tomb := tomb.Tomb{}
   364  	go pb.Run(&tomb)
   365  
   366  	assert.NoFileExists(t, "./out")
   367  
   368  	defer os.Remove("./out")
   369  
   370  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   371  	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
   372  	// make it wait a bit, CI can be slow
   373  	time.Sleep(time.Second)
   374  
   375  	content, err := os.ReadFile("./out")
   376  	require.NoError(t, err, "Error reading file")
   377  
   378  	decoder := json.NewDecoder(bytes.NewReader(content))
   379  
   380  	var alerts []models.Alert
   381  
   382  	// two notifications, one alert each
   383  
   384  	err = decoder.Decode(&alerts)
   385  	require.NoError(t, err)
   386  	assert.Len(t, alerts, 1)
   387  
   388  	err = decoder.Decode(&alerts)
   389  	require.NoError(t, err)
   390  	assert.Len(t, alerts, 1)
   391  
   392  	err = decoder.Decode(&alerts)
   393  	assert.Equal(t, err, io.EOF)
   394  }