github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/acquisition_test.go (about)

     1  package acquisition
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/prometheus/client_golang/prometheus"
    10  	log "github.com/sirupsen/logrus"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	tomb "gopkg.in/tomb.v2"
    14  	"gopkg.in/yaml.v2"
    15  
    16  	"github.com/crowdsecurity/go-cs-lib/cstest"
    17  
    18  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    19  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    20  	"github.com/crowdsecurity/crowdsec/pkg/types"
    21  )
    22  
    23  type MockSource struct {
    24  	configuration.DataSourceCommonCfg `yaml:",inline"`
    25  	Toto                              string `yaml:"toto"`
    26  	logger                            *log.Entry
    27  }
    28  
    29  func (f *MockSource) UnmarshalConfig(cfg []byte) error {
    30  	err := yaml.UnmarshalStrict(cfg, &f)
    31  	if err != nil {
    32  		return err
    33  	}
    34  
    35  	return nil
    36  }
    37  
    38  func (f *MockSource) Configure(cfg []byte, logger *log.Entry, metricsLevel int) error {
    39  	f.logger = logger
    40  	if err := f.UnmarshalConfig(cfg); err != nil {
    41  		return err
    42  	}
    43  
    44  	if f.Mode == "" {
    45  		f.Mode = configuration.CAT_MODE
    46  	}
    47  
    48  	if f.Mode != configuration.CAT_MODE && f.Mode != configuration.TAIL_MODE {
    49  		return fmt.Errorf("mode %s is not supported", f.Mode)
    50  	}
    51  
    52  	if f.Toto == "" {
    53  		return fmt.Errorf("expect non-empty toto")
    54  	}
    55  
    56  	return nil
    57  }
    58  func (f *MockSource) GetMode() string                                         { return f.Mode }
    59  func (f *MockSource) OneShotAcquisition(chan types.Event, *tomb.Tomb) error   { return nil }
    60  func (f *MockSource) StreamingAcquisition(chan types.Event, *tomb.Tomb) error { return nil }
    61  func (f *MockSource) CanRun() error                                           { return nil }
    62  func (f *MockSource) GetMetrics() []prometheus.Collector                      { return nil }
    63  func (f *MockSource) GetAggregMetrics() []prometheus.Collector                { return nil }
    64  func (f *MockSource) Dump() interface{}                                       { return f }
    65  func (f *MockSource) GetName() string                                         { return "mock" }
    66  func (f *MockSource) ConfigureByDSN(string, map[string]string, *log.Entry, string) error {
    67  	return fmt.Errorf("not supported")
    68  }
    69  func (f *MockSource) GetUuid() string { return "" }
    70  
    71  // copy the mocksource, but this one can't run
    72  type MockSourceCantRun struct {
    73  	MockSource
    74  }
    75  
    76  func (f *MockSourceCantRun) CanRun() error   { return fmt.Errorf("can't run bro") }
    77  func (f *MockSourceCantRun) GetName() string { return "mock_cant_run" }
    78  
    79  // appendMockSource is only used to add mock source for tests
    80  func appendMockSource() {
    81  	if GetDataSourceIface("mock") == nil {
    82  		AcquisitionSources["mock"] = func() DataSource { return &MockSource{} }
    83  	}
    84  
    85  	if GetDataSourceIface("mock_cant_run") == nil {
    86  		AcquisitionSources["mock_cant_run"] = func() DataSource { return &MockSourceCantRun{} }
    87  	}
    88  }
    89  
    90  func TestDataSourceConfigure(t *testing.T) {
    91  	appendMockSource()
    92  
    93  	tests := []struct {
    94  		TestName      string
    95  		String        string
    96  		ExpectedError string
    97  	}{
    98  		{
    99  			TestName: "basic_valid_config",
   100  			String: `
   101  mode: cat
   102  labels:
   103    test: foobar
   104  log_level: info
   105  source: mock
   106  toto: test_value1
   107  `,
   108  		},
   109  		{
   110  			TestName: "basic_debug_config",
   111  			String: `
   112  mode: cat
   113  labels:
   114    test: foobar
   115  log_level: debug
   116  source: mock
   117  toto: test_value1
   118  `,
   119  		},
   120  		{
   121  			TestName: "basic_tailmode_config",
   122  			String: `
   123  mode: tail
   124  labels:
   125    test: foobar
   126  log_level: debug
   127  source: mock
   128  toto: test_value1
   129  `,
   130  		},
   131  		{
   132  			TestName: "bad_mode_config",
   133  			String: `
   134  mode: ratata
   135  labels:
   136    test: foobar
   137  log_level: debug
   138  source: mock
   139  toto: test_value1
   140  `,
   141  			ExpectedError: "failed to configure datasource mock: mode ratata is not supported",
   142  		},
   143  		{
   144  			TestName: "bad_type_config",
   145  			String: `
   146  mode: cat
   147  labels:
   148    test: foobar
   149  log_level: debug
   150  source: tutu
   151  `,
   152  			ExpectedError: "cannot find source tutu",
   153  		},
   154  		{
   155  			TestName: "mismatch_config",
   156  			String: `
   157  mode: cat
   158  labels:
   159    test: foobar
   160  log_level: debug
   161  source: mock
   162  wowo: ajsajasjas
   163  `,
   164  			ExpectedError: "field wowo not found in type acquisition.MockSource",
   165  		},
   166  		{
   167  			TestName: "cant_run_error",
   168  			String: `
   169  mode: cat
   170  labels:
   171    test: foobar
   172  log_level: debug
   173  source: mock_cant_run
   174  wowo: ajsajasjas
   175  `,
   176  			ExpectedError: "datasource 'mock_cant_run' is not available: can't run bro",
   177  		},
   178  	}
   179  
   180  	for _, tc := range tests {
   181  		tc := tc
   182  		t.Run(tc.TestName, func(t *testing.T) {
   183  			common := configuration.DataSourceCommonCfg{}
   184  			yaml.Unmarshal([]byte(tc.String), &common)
   185  			ds, err := DataSourceConfigure(common, configuration.METRICS_NONE)
   186  			cstest.RequireErrorContains(t, err, tc.ExpectedError)
   187  			if tc.ExpectedError != "" {
   188  				return
   189  			}
   190  
   191  			switch tc.TestName {
   192  			case "basic_valid_config":
   193  				mock := (*ds).Dump().(*MockSource)
   194  				assert.Equal(t, "test_value1", mock.Toto)
   195  				assert.Equal(t, "cat", mock.Mode)
   196  				assert.Equal(t, log.InfoLevel, mock.logger.Logger.Level)
   197  				assert.Equal(t, map[string]string{"test": "foobar"}, mock.Labels)
   198  			case "basic_debug_config":
   199  				mock := (*ds).Dump().(*MockSource)
   200  				assert.Equal(t, "test_value1", mock.Toto)
   201  				assert.Equal(t, "cat", mock.Mode)
   202  				assert.Equal(t, log.DebugLevel, mock.logger.Logger.Level)
   203  				assert.Equal(t, map[string]string{"test": "foobar"}, mock.Labels)
   204  			case "basic_tailmode_config":
   205  				mock := (*ds).Dump().(*MockSource)
   206  				assert.Equal(t, "test_value1", mock.Toto)
   207  				assert.Equal(t, "tail", mock.Mode)
   208  				assert.Equal(t, log.DebugLevel, mock.logger.Logger.Level)
   209  				assert.Equal(t, map[string]string{"test": "foobar"}, mock.Labels)
   210  			}
   211  		})
   212  	}
   213  }
   214  
   215  func TestLoadAcquisitionFromFile(t *testing.T) {
   216  	appendMockSource()
   217  
   218  	tests := []struct {
   219  		TestName      string
   220  		Config        csconfig.CrowdsecServiceCfg
   221  		ExpectedError string
   222  		ExpectedLen   int
   223  	}{
   224  		{
   225  			TestName: "non_existent_file",
   226  			Config: csconfig.CrowdsecServiceCfg{
   227  				AcquisitionFiles: []string{"does_not_exist"},
   228  			},
   229  			ExpectedError: "open does_not_exist: " + cstest.FileNotFoundMessage,
   230  			ExpectedLen:   0,
   231  		},
   232  		{
   233  			TestName: "invalid_yaml_file",
   234  			Config: csconfig.CrowdsecServiceCfg{
   235  				AcquisitionFiles: []string{"test_files/badyaml.yaml"},
   236  			},
   237  			ExpectedError: "failed to yaml decode test_files/badyaml.yaml: yaml: unmarshal errors",
   238  			ExpectedLen:   0,
   239  		},
   240  		{
   241  			TestName: "invalid_empty_yaml",
   242  			Config: csconfig.CrowdsecServiceCfg{
   243  				AcquisitionFiles: []string{"test_files/emptyitem.yaml"},
   244  			},
   245  			ExpectedLen: 0,
   246  		},
   247  		{
   248  			TestName: "basic_valid",
   249  			Config: csconfig.CrowdsecServiceCfg{
   250  				AcquisitionFiles: []string{"test_files/basic_filemode.yaml"},
   251  			},
   252  			ExpectedLen: 2,
   253  		},
   254  		{
   255  			TestName: "missing_labels",
   256  			Config: csconfig.CrowdsecServiceCfg{
   257  				AcquisitionFiles: []string{"test_files/missing_labels.yaml"},
   258  			},
   259  			ExpectedError: "missing labels in test_files/missing_labels.yaml",
   260  		},
   261  		{
   262  			TestName: "backward_compat",
   263  			Config: csconfig.CrowdsecServiceCfg{
   264  				AcquisitionFiles: []string{"test_files/backward_compat.yaml"},
   265  			},
   266  			ExpectedLen: 2,
   267  		},
   268  		{
   269  			TestName: "bad_type",
   270  			Config: csconfig.CrowdsecServiceCfg{
   271  				AcquisitionFiles: []string{"test_files/bad_source.yaml"},
   272  			},
   273  			ExpectedError: "unknown data source does_not_exist in test_files/bad_source.yaml",
   274  		},
   275  		{
   276  			TestName: "invalid_filetype_config",
   277  			Config: csconfig.CrowdsecServiceCfg{
   278  				AcquisitionFiles: []string{"test_files/bad_filetype.yaml"},
   279  			},
   280  			ExpectedError: "while configuring datasource of type file from test_files/bad_filetype.yaml",
   281  		},
   282  	}
   283  	for _, tc := range tests {
   284  		tc := tc
   285  		t.Run(tc.TestName, func(t *testing.T) {
   286  			dss, err := LoadAcquisitionFromFile(&tc.Config, nil)
   287  			cstest.RequireErrorContains(t, err, tc.ExpectedError)
   288  			if tc.ExpectedError != "" {
   289  				return
   290  			}
   291  
   292  			assert.Len(t, dss, tc.ExpectedLen)
   293  		})
   294  	}
   295  }
   296  
   297  /*
   298   test start acquisition :
   299    - create mock parser in cat mode : start acquisition, check it returns, count items in chan
   300    - create mock parser in tail mode : start acquisition, sleep, check item count, tomb kill it, wait for it to return
   301  */
   302  
   303  type MockCat struct {
   304  	configuration.DataSourceCommonCfg `yaml:",inline"`
   305  	logger                            *log.Entry
   306  }
   307  
   308  func (f *MockCat) Configure(cfg []byte, logger *log.Entry, metricsLevel int) error {
   309  	f.logger = logger
   310  	if f.Mode == "" {
   311  		f.Mode = configuration.CAT_MODE
   312  	}
   313  
   314  	if f.Mode != configuration.CAT_MODE {
   315  		return fmt.Errorf("mode %s is not supported", f.Mode)
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  func (f *MockCat) UnmarshalConfig(cfg []byte) error { return nil }
   322  func (f *MockCat) GetName() string                  { return "mock_cat" }
   323  func (f *MockCat) GetMode() string                  { return "cat" }
   324  func (f *MockCat) OneShotAcquisition(out chan types.Event, tomb *tomb.Tomb) error {
   325  	for i := 0; i < 10; i++ {
   326  		evt := types.Event{}
   327  		evt.Line.Src = "test"
   328  		out <- evt
   329  	}
   330  
   331  	return nil
   332  }
   333  func (f *MockCat) StreamingAcquisition(chan types.Event, *tomb.Tomb) error {
   334  	return fmt.Errorf("can't run in tail")
   335  }
   336  func (f *MockCat) CanRun() error                            { return nil }
   337  func (f *MockCat) GetMetrics() []prometheus.Collector       { return nil }
   338  func (f *MockCat) GetAggregMetrics() []prometheus.Collector { return nil }
   339  func (f *MockCat) Dump() interface{}                        { return f }
   340  func (f *MockCat) ConfigureByDSN(string, map[string]string, *log.Entry, string) error {
   341  	return fmt.Errorf("not supported")
   342  }
   343  func (f *MockCat) GetUuid() string { return "" }
   344  
   345  //----
   346  
   347  type MockTail struct {
   348  	configuration.DataSourceCommonCfg `yaml:",inline"`
   349  	logger                            *log.Entry
   350  }
   351  
   352  func (f *MockTail) Configure(cfg []byte, logger *log.Entry, metricsLevel int) error {
   353  	f.logger = logger
   354  	if f.Mode == "" {
   355  		f.Mode = configuration.TAIL_MODE
   356  	}
   357  
   358  	if f.Mode != configuration.TAIL_MODE {
   359  		return fmt.Errorf("mode %s is not supported", f.Mode)
   360  	}
   361  
   362  	return nil
   363  }
   364  
   365  func (f *MockTail) UnmarshalConfig(cfg []byte) error { return nil }
   366  func (f *MockTail) GetName() string                  { return "mock_tail" }
   367  func (f *MockTail) GetMode() string                  { return "tail" }
   368  func (f *MockTail) OneShotAcquisition(out chan types.Event, tomb *tomb.Tomb) error {
   369  	return fmt.Errorf("can't run in cat mode")
   370  }
   371  func (f *MockTail) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   372  	for i := 0; i < 10; i++ {
   373  		evt := types.Event{}
   374  		evt.Line.Src = "test"
   375  		out <- evt
   376  	}
   377  	<-t.Dying()
   378  
   379  	return nil
   380  }
   381  func (f *MockTail) CanRun() error                            { return nil }
   382  func (f *MockTail) GetMetrics() []prometheus.Collector       { return nil }
   383  func (f *MockTail) GetAggregMetrics() []prometheus.Collector { return nil }
   384  func (f *MockTail) Dump() interface{}                        { return f }
   385  func (f *MockTail) ConfigureByDSN(string, map[string]string, *log.Entry, string) error {
   386  	return fmt.Errorf("not supported")
   387  }
   388  func (f *MockTail) GetUuid() string { return "" }
   389  
   390  //func StartAcquisition(sources []DataSource, output chan types.Event, AcquisTomb *tomb.Tomb) error {
   391  
   392  func TestStartAcquisitionCat(t *testing.T) {
   393  	sources := []DataSource{
   394  		&MockCat{},
   395  	}
   396  	out := make(chan types.Event)
   397  	acquisTomb := tomb.Tomb{}
   398  
   399  	go func() {
   400  		if err := StartAcquisition(sources, out, &acquisTomb); err != nil {
   401  			t.Errorf("unexpected error")
   402  		}
   403  	}()
   404  
   405  	count := 0
   406  READLOOP:
   407  	for {
   408  		select {
   409  		case <-out:
   410  			count++
   411  		case <-time.After(1 * time.Second):
   412  			break READLOOP
   413  		}
   414  	}
   415  
   416  	assert.Equal(t, 10, count)
   417  }
   418  
   419  func TestStartAcquisitionTail(t *testing.T) {
   420  	sources := []DataSource{
   421  		&MockTail{},
   422  	}
   423  	out := make(chan types.Event)
   424  	acquisTomb := tomb.Tomb{}
   425  
   426  	go func() {
   427  		if err := StartAcquisition(sources, out, &acquisTomb); err != nil {
   428  			t.Errorf("unexpected error")
   429  		}
   430  	}()
   431  
   432  	count := 0
   433  READLOOP:
   434  	for {
   435  		select {
   436  		case <-out:
   437  			count++
   438  		case <-time.After(1 * time.Second):
   439  			break READLOOP
   440  		}
   441  	}
   442  
   443  	assert.Equal(t, 10, count)
   444  
   445  	acquisTomb.Kill(nil)
   446  	time.Sleep(1 * time.Second)
   447  	require.NoError(t, acquisTomb.Err(), "tomb is not dead")
   448  }
   449  
   450  type MockTailError struct {
   451  	MockTail
   452  }
   453  
   454  func (f *MockTailError) StreamingAcquisition(out chan types.Event, t *tomb.Tomb) error {
   455  	for i := 0; i < 10; i++ {
   456  		evt := types.Event{}
   457  		evt.Line.Src = "test"
   458  		out <- evt
   459  	}
   460  	t.Kill(fmt.Errorf("got error (tomb)"))
   461  
   462  	return fmt.Errorf("got error")
   463  }
   464  
   465  func TestStartAcquisitionTailError(t *testing.T) {
   466  	sources := []DataSource{
   467  		&MockTailError{},
   468  	}
   469  	out := make(chan types.Event)
   470  	acquisTomb := tomb.Tomb{}
   471  
   472  	go func() {
   473  		if err := StartAcquisition(sources, out, &acquisTomb); err != nil && err.Error() != "got error (tomb)" {
   474  			t.Errorf("expected error, got '%s'", err)
   475  		}
   476  	}()
   477  
   478  	count := 0
   479  READLOOP:
   480  	for {
   481  		select {
   482  		case <-out:
   483  			count++
   484  		case <-time.After(1 * time.Second):
   485  			break READLOOP
   486  		}
   487  	}
   488  	assert.Equal(t, 10, count)
   489  	//acquisTomb.Kill(nil)
   490  	time.Sleep(1 * time.Second)
   491  	cstest.RequireErrorContains(t, acquisTomb.Err(), "got error (tomb)")
   492  }
   493  
   494  type MockSourceByDSN struct {
   495  	configuration.DataSourceCommonCfg `yaml:",inline"`
   496  	Toto                              string     `yaml:"toto"`
   497  	logger                            *log.Entry //nolint: unused
   498  }
   499  
   500  func (f *MockSourceByDSN) UnmarshalConfig(cfg []byte) error { return nil }
   501  func (f *MockSourceByDSN) Configure(cfg []byte, logger *log.Entry, metricsLevel int) error {
   502  	return nil
   503  }
   504  func (f *MockSourceByDSN) GetMode() string                                         { return f.Mode }
   505  func (f *MockSourceByDSN) OneShotAcquisition(chan types.Event, *tomb.Tomb) error   { return nil }
   506  func (f *MockSourceByDSN) StreamingAcquisition(chan types.Event, *tomb.Tomb) error { return nil }
   507  func (f *MockSourceByDSN) CanRun() error                                           { return nil }
   508  func (f *MockSourceByDSN) GetMetrics() []prometheus.Collector                      { return nil }
   509  func (f *MockSourceByDSN) GetAggregMetrics() []prometheus.Collector                { return nil }
   510  func (f *MockSourceByDSN) Dump() interface{}                                       { return f }
   511  func (f *MockSourceByDSN) GetName() string                                         { return "mockdsn" }
   512  func (f *MockSourceByDSN) ConfigureByDSN(dsn string, labels map[string]string, logger *log.Entry, uuid string) error {
   513  	dsn = strings.TrimPrefix(dsn, "mockdsn://")
   514  	if dsn != "test_expect" {
   515  		return fmt.Errorf("unexpected value")
   516  	}
   517  
   518  	return nil
   519  }
   520  func (f *MockSourceByDSN) GetUuid() string { return "" }
   521  
   522  func TestConfigureByDSN(t *testing.T) {
   523  	tests := []struct {
   524  		dsn            string
   525  		ExpectedError  string
   526  		ExpectedResLen int
   527  	}{
   528  		{
   529  			dsn:           "baddsn",
   530  			ExpectedError: "baddsn isn't valid dsn (no protocol)",
   531  		},
   532  		{
   533  			dsn:           "foobar://toto",
   534  			ExpectedError: "no acquisition for protocol foobar://",
   535  		},
   536  		{
   537  			dsn:            "mockdsn://test_expect",
   538  			ExpectedResLen: 1,
   539  		},
   540  		{
   541  			dsn:           "mockdsn://bad",
   542  			ExpectedError: "unexpected value",
   543  		},
   544  	}
   545  
   546  	if GetDataSourceIface("mockdsn") == nil {
   547  		AcquisitionSources["mockdsn"] = func() DataSource { return &MockSourceByDSN{} }
   548  	}
   549  
   550  	for _, tc := range tests {
   551  		tc := tc
   552  		t.Run(tc.dsn, func(t *testing.T) {
   553  			srcs, err := LoadAcquisitionFromDSN(tc.dsn, map[string]string{"type": "test_label"}, "")
   554  			cstest.RequireErrorContains(t, err, tc.ExpectedError)
   555  
   556  			assert.Len(t, srcs, tc.ExpectedResLen)
   557  		})
   558  	}
   559  }