github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/file/file_test.go (about)

     1  package fileacquisition_test
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"runtime"
     7  	"testing"
     8  	"time"
     9  
    10  	log "github.com/sirupsen/logrus"
    11  	"github.com/sirupsen/logrus/hooks/test"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	"gopkg.in/tomb.v2"
    15  
    16  	"github.com/crowdsecurity/go-cs-lib/cstest"
    17  
    18  	"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
    19  	fileacquisition "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/file"
    20  	"github.com/crowdsecurity/crowdsec/pkg/types"
    21  )
    22  
    23  func TestBadConfiguration(t *testing.T) {
    24  	tests := []struct {
    25  		name        string
    26  		config      string
    27  		expectedErr string
    28  	}{
    29  		{
    30  			name:        "extra configuration key",
    31  			config:      "foobar: asd.log",
    32  			expectedErr: "line 1: field foobar not found in type fileacquisition.FileConfiguration",
    33  		},
    34  		{
    35  			name:        "missing filenames",
    36  			config:      "mode: tail",
    37  			expectedErr: "no filename or filenames configuration provided",
    38  		},
    39  		{
    40  			name:        "glob syntax error",
    41  			config:      `filename: "[asd-.log"`,
    42  			expectedErr: "glob failure: syntax error in pattern",
    43  		},
    44  		{
    45  			name: "bad exclude regexp",
    46  			config: `filenames: ["asd.log"]
    47  exclude_regexps: ["as[a-$d"]`,
    48  			expectedErr: "could not compile regexp as",
    49  		},
    50  	}
    51  
    52  	subLogger := log.WithFields(log.Fields{
    53  		"type": "file",
    54  	})
    55  
    56  	for _, tc := range tests {
    57  		tc := tc
    58  		t.Run(tc.name, func(t *testing.T) {
    59  			f := fileacquisition.FileSource{}
    60  			err := f.Configure([]byte(tc.config), subLogger, configuration.METRICS_NONE)
    61  			cstest.RequireErrorContains(t, err, tc.expectedErr)
    62  		})
    63  	}
    64  }
    65  
    66  func TestConfigureDSN(t *testing.T) {
    67  	file := "/etc/passwd"
    68  
    69  	if runtime.GOOS == "windows" {
    70  		file = `C:\Windows\System32\drivers\etc\hosts`
    71  	}
    72  
    73  	tests := []struct {
    74  		dsn         string
    75  		expectedErr string
    76  	}{
    77  		{
    78  			dsn:         "asd://",
    79  			expectedErr: "invalid DSN asd:// for file source, must start with file://",
    80  		},
    81  		{
    82  			dsn:         "file://",
    83  			expectedErr: "empty file:// DSN",
    84  		},
    85  		{
    86  			dsn: fmt.Sprintf("file://%s?log_level=warn", file),
    87  		},
    88  		{
    89  			dsn:         fmt.Sprintf("file://%s?log_level=foobar", file),
    90  			expectedErr: "unknown level foobar: not a valid logrus Level:",
    91  		},
    92  	}
    93  
    94  	subLogger := log.WithFields(log.Fields{
    95  		"type": "file",
    96  	})
    97  
    98  	for _, tc := range tests {
    99  		tc := tc
   100  		t.Run(tc.dsn, func(t *testing.T) {
   101  			f := fileacquisition.FileSource{}
   102  			err := f.ConfigureByDSN(tc.dsn, map[string]string{"type": "testtype"}, subLogger, "")
   103  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   104  		})
   105  	}
   106  }
   107  
   108  func TestOneShot(t *testing.T) {
   109  	permDeniedFile := "/etc/shadow"
   110  	permDeniedError := "failed opening /etc/shadow: open /etc/shadow: permission denied"
   111  
   112  	if runtime.GOOS == "windows" {
   113  		// Technically, this is not a permission denied error, but we just want to test what happens
   114  		// if we do not have access to the file
   115  		permDeniedFile = `C:\Windows\System32\config\SAM`
   116  		permDeniedError = `failed opening C:\Windows\System32\config\SAM: open C:\Windows\System32\config\SAM: The process cannot access the file because it is being used by another process.`
   117  	}
   118  
   119  	tests := []struct {
   120  		name              string
   121  		config            string
   122  		expectedConfigErr string
   123  		expectedErr       string
   124  		expectedOutput    string
   125  		expectedLines     int
   126  		logLevel          log.Level
   127  		setup             func()
   128  		afterConfigure    func()
   129  		teardown          func()
   130  	}{
   131  		{
   132  			name: "permission denied",
   133  			config: fmt.Sprintf(`
   134  mode: cat
   135  filename: %s`, permDeniedFile),
   136  			expectedErr:   permDeniedError,
   137  			logLevel:      log.WarnLevel,
   138  			expectedLines: 0,
   139  		},
   140  		{
   141  			name: "ignored directory",
   142  			config: `
   143  mode: cat
   144  filename: /`,
   145  			expectedOutput: "/ is a directory, ignoring it",
   146  			logLevel:       log.WarnLevel,
   147  			expectedLines:  0,
   148  		},
   149  		{
   150  			name: "glob syntax error",
   151  			config: `
   152  mode: cat
   153  filename: "[*-.log"`,
   154  			expectedConfigErr: "glob failure: syntax error in pattern",
   155  			logLevel:          log.WarnLevel,
   156  			expectedLines:     0,
   157  		},
   158  		{
   159  			name: "no matching files",
   160  			config: `
   161  mode: cat
   162  filename: /do/not/exist`,
   163  			expectedOutput: "No matching files for pattern /do/not/exist",
   164  			logLevel:       log.WarnLevel,
   165  			expectedLines:  0,
   166  		},
   167  		{
   168  			name: "test.log",
   169  			config: `
   170  mode: cat
   171  filename: test_files/test.log`,
   172  			expectedLines: 5,
   173  			logLevel:      log.WarnLevel,
   174  		},
   175  		{
   176  			name: "test.log.gz",
   177  			config: `
   178  mode: cat
   179  filename: test_files/test.log.gz`,
   180  			expectedLines: 5,
   181  			logLevel:      log.WarnLevel,
   182  		},
   183  		{
   184  			name: "unexpected end of gzip stream",
   185  			config: `
   186  mode: cat
   187  filename: test_files/bad.gz`,
   188  			expectedErr:   "failed to read gz test_files/bad.gz: unexpected EOF",
   189  			expectedLines: 0,
   190  			logLevel:      log.WarnLevel,
   191  		},
   192  		{
   193  			name: "deleted file",
   194  			config: `
   195  mode: cat
   196  filename: test_files/test_delete.log`,
   197  			setup: func() {
   198  				f, _ := os.Create("test_files/test_delete.log")
   199  				f.Close()
   200  			},
   201  			afterConfigure: func() {
   202  				os.Remove("test_files/test_delete.log")
   203  			},
   204  			expectedErr: "could not stat file test_files/test_delete.log",
   205  		},
   206  	}
   207  
   208  	for _, tc := range tests {
   209  		tc := tc
   210  		t.Run(tc.name, func(t *testing.T) {
   211  			logger, hook := test.NewNullLogger()
   212  			logger.SetLevel(tc.logLevel)
   213  
   214  			subLogger := logger.WithFields(log.Fields{
   215  				"type": "file",
   216  			})
   217  
   218  			tomb := tomb.Tomb{}
   219  			out := make(chan types.Event, 100)
   220  			f := fileacquisition.FileSource{}
   221  
   222  			if tc.setup != nil {
   223  				tc.setup()
   224  			}
   225  
   226  			err := f.Configure([]byte(tc.config), subLogger, configuration.METRICS_NONE)
   227  			cstest.RequireErrorContains(t, err, tc.expectedConfigErr)
   228  			if tc.expectedConfigErr != "" {
   229  				return
   230  			}
   231  
   232  			if tc.afterConfigure != nil {
   233  				tc.afterConfigure()
   234  			}
   235  			err = f.OneShotAcquisition(out, &tomb)
   236  			actualLines := len(out)
   237  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   238  
   239  			if tc.expectedLines != 0 {
   240  				assert.Equal(t, tc.expectedLines, actualLines)
   241  			}
   242  
   243  			if tc.expectedOutput != "" {
   244  				assert.Contains(t, hook.LastEntry().Message, tc.expectedOutput)
   245  				hook.Reset()
   246  			}
   247  			if tc.teardown != nil {
   248  				tc.teardown()
   249  			}
   250  		})
   251  	}
   252  }
   253  
   254  func TestLiveAcquisition(t *testing.T) {
   255  	permDeniedFile := "/etc/shadow"
   256  	permDeniedError := "unable to read /etc/shadow : open /etc/shadow: permission denied"
   257  	testPattern := "test_files/*.log"
   258  
   259  	if runtime.GOOS == "windows" {
   260  		// Technically, this is not a permission denied error, but we just want to test what happens
   261  		// if we do not have access to the file
   262  		permDeniedFile = `C:\Windows\System32\config\SAM`
   263  		permDeniedError = `unable to read C:\Windows\System32\config\SAM : open C:\Windows\System32\config\SAM: The process cannot access the file because it is being used by another process`
   264  		testPattern = `test_files\*.log`
   265  	}
   266  
   267  	tests := []struct {
   268  		name           string
   269  		config         string
   270  		expectedErr    string
   271  		expectedOutput string
   272  		expectedLines  int
   273  		logLevel       log.Level
   274  		setup          func()
   275  		afterConfigure func()
   276  		teardown       func()
   277  	}{
   278  		{
   279  			config: fmt.Sprintf(`
   280  mode: tail
   281  filename: %s`, permDeniedFile),
   282  			expectedOutput: permDeniedError,
   283  			logLevel:       log.InfoLevel,
   284  			expectedLines:  0,
   285  			name:           "PermissionDenied",
   286  		},
   287  		{
   288  			config: `
   289  mode: tail
   290  filename: /`,
   291  			expectedOutput: "/ is a directory, ignoring it",
   292  			logLevel:       log.WarnLevel,
   293  			expectedLines:  0,
   294  			name:           "Directory",
   295  		},
   296  		{
   297  			config: `
   298  mode: tail
   299  filename: /do/not/exist`,
   300  			expectedOutput: "No matching files for pattern /do/not/exist",
   301  			logLevel:       log.WarnLevel,
   302  			expectedLines:  0,
   303  			name:           "badPattern",
   304  		},
   305  		{
   306  			config: fmt.Sprintf(`
   307  mode: tail
   308  filenames:
   309   - %s
   310  force_inotify: true`, testPattern),
   311  			expectedLines: 5,
   312  			logLevel:      log.DebugLevel,
   313  			name:          "basicGlob",
   314  		},
   315  		{
   316  			config: fmt.Sprintf(`
   317  mode: tail
   318  filenames:
   319   - %s
   320  force_inotify: true`, testPattern),
   321  			expectedLines: 0,
   322  			logLevel:      log.DebugLevel,
   323  			name:          "GlobInotify",
   324  			afterConfigure: func() {
   325  				f, _ := os.Create("test_files/a.log")
   326  				f.Close()
   327  				time.Sleep(1 * time.Second)
   328  				os.Remove("test_files/a.log")
   329  			},
   330  		},
   331  		{
   332  			config: fmt.Sprintf(`
   333  mode: tail
   334  filenames:
   335   - %s
   336  force_inotify: true`, testPattern),
   337  			expectedLines: 5,
   338  			logLevel:      log.DebugLevel,
   339  			name:          "GlobInotifyChmod",
   340  			afterConfigure: func() {
   341  				f, _ := os.Create("test_files/a.log")
   342  				f.Close()
   343  				time.Sleep(1 * time.Second)
   344  				os.Chmod("test_files/a.log", 0o000)
   345  			},
   346  			teardown: func() {
   347  				os.Chmod("test_files/a.log", 0o644)
   348  				os.Remove("test_files/a.log")
   349  			},
   350  		},
   351  		{
   352  			config: fmt.Sprintf(`
   353  mode: tail
   354  filenames:
   355   - %s
   356  force_inotify: true`, testPattern),
   357  			expectedLines: 5,
   358  			logLevel:      log.DebugLevel,
   359  			name:          "InotifyMkDir",
   360  			afterConfigure: func() {
   361  				os.Mkdir("test_files/pouet/", 0o700)
   362  			},
   363  			teardown: func() {
   364  				os.Remove("test_files/pouet/")
   365  			},
   366  		},
   367  	}
   368  
   369  	for _, tc := range tests {
   370  		tc := tc
   371  		t.Run(tc.name, func(t *testing.T) {
   372  			logger, hook := test.NewNullLogger()
   373  			logger.SetLevel(tc.logLevel)
   374  
   375  			subLogger := logger.WithFields(log.Fields{
   376  				"type": "file",
   377  			})
   378  
   379  			tomb := tomb.Tomb{}
   380  			out := make(chan types.Event)
   381  
   382  			f := fileacquisition.FileSource{}
   383  
   384  			if tc.setup != nil {
   385  				tc.setup()
   386  			}
   387  
   388  			err := f.Configure([]byte(tc.config), subLogger, configuration.METRICS_NONE)
   389  			require.NoError(t, err)
   390  
   391  			if tc.afterConfigure != nil {
   392  				tc.afterConfigure()
   393  			}
   394  
   395  			actualLines := 0
   396  			if tc.expectedLines != 0 {
   397  				go func() {
   398  					for {
   399  						select {
   400  						case <-out:
   401  							actualLines++
   402  						case <-time.After(2 * time.Second):
   403  							return
   404  						}
   405  					}
   406  				}()
   407  			}
   408  
   409  			err = f.StreamingAcquisition(out, &tomb)
   410  			cstest.RequireErrorContains(t, err, tc.expectedErr)
   411  
   412  			if tc.expectedLines != 0 {
   413  				fd, err := os.Create("test_files/stream.log")
   414  				require.NoError(t, err, "could not create test file")
   415  
   416  				for i := 0; i < 5; i++ {
   417  					_, err = fmt.Fprintf(fd, "%d\n", i)
   418  					if err != nil {
   419  						t.Fatalf("could not write test file : %s", err)
   420  						os.Remove("test_files/stream.log")
   421  					}
   422  				}
   423  
   424  				fd.Close()
   425  				// we sleep to make sure we detect the new file
   426  				time.Sleep(3 * time.Second)
   427  				os.Remove("test_files/stream.log")
   428  				assert.Equal(t, tc.expectedLines, actualLines)
   429  			}
   430  
   431  			if tc.expectedOutput != "" {
   432  				if hook.LastEntry() == nil {
   433  					t.Fatalf("expected output %s, but got nothing", tc.expectedOutput)
   434  				}
   435  
   436  				assert.Contains(t, hook.LastEntry().Message, tc.expectedOutput)
   437  				hook.Reset()
   438  			}
   439  
   440  			if tc.teardown != nil {
   441  				tc.teardown()
   442  			}
   443  
   444  			tomb.Kill(nil)
   445  		})
   446  	}
   447  }
   448  
   449  func TestExclusion(t *testing.T) {
   450  	config := `filenames: ["test_files/*.log*"]
   451  exclude_regexps: ["\\.gz$"]`
   452  	logger, hook := test.NewNullLogger()
   453  	// logger.SetLevel(ts.logLevel)
   454  	subLogger := logger.WithFields(log.Fields{
   455  		"type": "file",
   456  	})
   457  
   458  	f := fileacquisition.FileSource{}
   459  	if err := f.Configure([]byte(config), subLogger, configuration.METRICS_NONE); err != nil {
   460  		subLogger.Fatalf("unexpected error: %s", err)
   461  	}
   462  
   463  	expectedLogOutput := "Skipping file test_files/test.log.gz as it matches exclude pattern"
   464  
   465  	if runtime.GOOS == "windows" {
   466  		expectedLogOutput = `Skipping file test_files\test.log.gz as it matches exclude pattern \.gz`
   467  	}
   468  
   469  	if hook.LastEntry() == nil {
   470  		t.Fatalf("expected output %s, but got nothing", expectedLogOutput)
   471  	}
   472  
   473  	assert.Contains(t, hook.LastEntry().Message, expectedLogOutput)
   474  	hook.Reset()
   475  }