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 }