瀏覽代碼

functional tests, minor refactoring and lint/cleanup (#1570)

* cmd/crowdsec: removed log.Fatal()s, added tests and print error for unrecognized argument
* updated golangci-lint to v1.46
* lint/deadcode: fix existing issues
* tests: cscli config backup/restore
* tests: cscli completion powershell/fish
* err check: pflags MarkHidden()
* empty .dockerignore (and explain the reason)
* tests, errors.Wrap
* test for CS_LAPI_SECRET and minor refactoring
* minor style changes
* log cleanup
mmetc 3 年之前
父節點
當前提交
799cc82bb5
共有 48 個文件被更改,包括 383 次插入273 次删除
  1. 3 0
      .dockerignore
  2. 3 3
      .github/workflows/ci_golangci-lint-windows.yml
  3. 1 2
      .github/workflows/ci_golangci-lint.yml
  4. 10 16
      .golangci.yml
  5. 5 3
      cmd/crowdsec-cli/capi.go
  6. 5 6
      cmd/crowdsec-cli/collections.go
  7. 6 6
      cmd/crowdsec-cli/config.go
  8. 0 5
      cmd/crowdsec-cli/configfile.go
  9. 0 6
      cmd/crowdsec-cli/configfile_windows.go
  10. 2 2
      cmd/crowdsec-cli/console.go
  11. 5 5
      cmd/crowdsec-cli/hub.go
  12. 2 2
      cmd/crowdsec-cli/lapi.go
  13. 1 1
      cmd/crowdsec-cli/main.go
  14. 7 12
      cmd/crowdsec-cli/messages.go
  15. 2 2
      cmd/crowdsec-cli/parsers.go
  16. 2 2
      cmd/crowdsec-cli/postoverflows.go
  17. 2 2
      cmd/crowdsec-cli/scenarios.go
  18. 8 8
      cmd/crowdsec-cli/simulation.go
  19. 1 1
      cmd/crowdsec-cli/utils.go
  20. 4 5
      cmd/crowdsec/api.go
  21. 0 5
      cmd/crowdsec/configfile.go
  22. 0 3
      cmd/crowdsec/configfile_windows.go
  23. 40 6
      cmd/crowdsec/main.go
  24. 4 18
      cmd/crowdsec/run_in_svc.go
  25. 17 32
      cmd/crowdsec/run_in_svc_windows.go
  26. 27 24
      cmd/crowdsec/serve.go
  27. 13 10
      cmd/crowdsec/win_service.go
  28. 1 0
      pkg/acquisition/acquisition_test.go
  29. 24 21
      pkg/acquisition/modules/file/file.go
  30. 4 4
      pkg/apiclient/alerts_service_test.go
  31. 4 4
      pkg/apiclient/auth_service_test.go
  32. 1 1
      pkg/apiclient/auth_test.go
  33. 5 5
      pkg/apiclient/decisions_service_test.go
  34. 1 1
      pkg/apiserver/api_key_test.go
  35. 2 2
      pkg/apiserver/apiserver.go
  36. 4 4
      pkg/apiserver/machines_test.go
  37. 29 16
      pkg/apiserver/middlewares/v1/jwt.go
  38. 0 9
      pkg/apiserver/testutils.go
  39. 0 7
      pkg/apiserver/testutils_windows.go
  40. 1 1
      pkg/csconfig/crowdsec_service.go
  41. 4 4
      pkg/csconfig/hub_test.go
  42. 0 1
      pkg/cwhub/cwhub_test.go
  43. 1 1
      pkg/parser/parsing_test.go
  44. 1 1
      pkg/parser/runtime.go
  45. 37 4
      tests/bats/01_base.bats
  46. 57 0
      tests/bats/01_crowdsec.bats
  47. 5 0
      tests/bats/06_crowdsec.bats
  48. 32 0
      tests/bats/72_plugin_badconfig.bats

+ 3 - 0
.dockerignore

@@ -0,0 +1,3 @@
+# We include .git in the build context because excluding it would break the
+# "make release" target, which uses git to retrieve the build version and tag.
+#.git

+ 3 - 3
.github/workflows/ci_golangci-lint-windows.yml

@@ -19,12 +19,12 @@ jobs:
     name: lint-windows
     name: lint-windows
     runs-on: windows-2022
     runs-on: windows-2022
     steps:
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: golangci-lint
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+        uses: golangci/golangci-lint-action@v3
         with:
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
-          version: v1.45.2
+          version: v1.46
           # Optional: golangci-lint command line arguments.
           # Optional: golangci-lint command line arguments.
           args: --issues-exit-code=0 --timeout 10m
           args: --issues-exit-code=0 --timeout 10m
           only-new-issues: true
           only-new-issues: true

+ 1 - 2
.github/workflows/ci_golangci-lint.yml

@@ -19,13 +19,12 @@ jobs:
     name: lint
     name: lint
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/setup-go@v3
       - uses: actions/checkout@v3
       - uses: actions/checkout@v3
       - name: golangci-lint
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v3
         uses: golangci/golangci-lint-action@v3
         with:
         with:
           # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
           # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
-          version: v1.45
+          version: v1.46
           # Optional: golangci-lint command line arguments.
           # Optional: golangci-lint command line arguments.
           args: --issues-exit-code=1 --timeout 5m
           args: --issues-exit-code=1 --timeout 5m
           # Optional: show only new issues if it's a pull request. The default value is `false`.
           # Optional: show only new issues if it's a pull request. The default value is `false`.

+ 10 - 16
.golangci.yml

@@ -38,10 +38,11 @@ linters:
     #
     #
     # DEPRECATED by golangi-lint
     # DEPRECATED by golangi-lint
     #
     #
-    - golint          # [deprecated]: Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
-    - interfacer      # [deprecated]: Linter that suggests narrower interface types
-    - maligned        # [deprecated]: Tool to detect Go structs that would take less memory if their fields were sorted
-    - scopelint       # [deprecated]: Scopelint checks for unpinned variables in go programs
+    - exhaustivestruct  # The owner seems to have abandoned the linter. Replaced by exhaustruct.
+    - golint            # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
+    - interfacer        # Linter that suggests narrower interface types
+    - maligned          # Tool to detect Go structs that would take less memory if their fields were sorted
+    - scopelint         # Scopelint checks for unpinned variables in go programs
 
 
     #
     #
     # Enabled
     # Enabled
@@ -96,6 +97,8 @@ linters:
     - misspell              # Finds commonly misspelled English words in comments
     - misspell              # Finds commonly misspelled English words in comments
     - nakedret              # Finds naked returns in functions greater than a specified function length
     - nakedret              # Finds naked returns in functions greater than a specified function length
     - nilerr                # Finds the code that returns nil even if it checks that the error is not nil.
     - nilerr                # Finds the code that returns nil even if it checks that the error is not nil.
+    - nonamedreturns        # Reports all named returns
+    - nosprintfhostport   # Checks for misuse of Sprintf to construct a host with port in a URL.
     - predeclared           # find code that shadows one of Go's predeclared identifiers
     - predeclared           # find code that shadows one of Go's predeclared identifiers
     - promlinter            # Check Prometheus metrics naming via promlint
     - promlinter            # Check Prometheus metrics naming via promlint
     - revive                # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
     - revive                # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
@@ -148,13 +151,14 @@ linters:
     - testpackage           # linter that makes you use a separate _test package
     - testpackage           # linter that makes you use a separate _test package
 
 
     #
     #
-    # Too strict (for now?)
+    # Too strict / too many false positives (for now?)
     #
     #
+    - execinquery           # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds
     - forbidigo             # Forbids identifiers
     - forbidigo             # Forbids identifiers
     - tagliatelle           # Checks the struct tags.
     - tagliatelle           # Checks the struct tags.
     - varnamelen            # checks that the length of a variable's name matches its scope
     - varnamelen            # checks that the length of a variable's name matches its scope
     - gochecknoglobals      # check that no global variables exist
     - gochecknoglobals      # check that no global variables exist
-    - exhaustivestruct      # Checks if all struct's fields are initialized
+    - exhaustruct           # Checks if all structure fields are initialized
     - goconst               # Finds repeated strings that could be replaced by a constant
     - goconst               # Finds repeated strings that could be replaced by a constant
     - stylecheck            # Stylecheck is a replacement for golint
     - stylecheck            # Stylecheck is a replacement for golint
 
 
@@ -228,16 +232,6 @@ issues:
         - gosimple
         - gosimple
       text: "S1028: should use .* instead of .*"
       text: "S1028: should use .* instead of .*"
 
 
-    #
-    # deadcode
-    #
-
-    - linters:
-        - deadcode
-        - unused
-        - structcheck
-      text: ".* is unused"
-
     #
     #
     # ineffassign
     # ineffassign
     #
     #

+ 5 - 3
cmd/crowdsec-cli/capi.go

@@ -103,7 +103,9 @@ func NewCapiCmd() *cobra.Command {
 	}
 	}
 	cmdCapiRegister.Flags().StringVarP(&outputFile, "file", "f", "", "output file destination")
 	cmdCapiRegister.Flags().StringVarP(&outputFile, "file", "f", "", "output file destination")
 	cmdCapiRegister.Flags().StringVar(&capiUserPrefix, "schmilblick", "", "set a schmilblick (use in tests only)")
 	cmdCapiRegister.Flags().StringVar(&capiUserPrefix, "schmilblick", "", "set a schmilblick (use in tests only)")
-	cmdCapiRegister.Flags().MarkHidden("schmilblick")
+	if err := cmdCapiRegister.Flags().MarkHidden("schmilblick"); err != nil {
+		log.Fatalf("failed to hide flag: %s", err)
+	}
 	cmdCapi.AddCommand(cmdCapiRegister)
 	cmdCapi.AddCommand(cmdCapiRegister)
 
 
 	var cmdCapiStatus = &cobra.Command{
 	var cmdCapiStatus = &cobra.Command{
@@ -131,11 +133,11 @@ func NewCapiCmd() *cobra.Command {
 			}
 			}
 
 
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to load hub index : %s", err)
 				log.Fatalf("Failed to load hub index : %s", err)
 			}
 			}
 			scenarios, err := cwhub.GetInstalledScenariosAsString()
 			scenarios, err := cwhub.GetInstalledScenariosAsString()

+ 5 - 6
cmd/crowdsec-cli/collections.go

@@ -21,7 +21,7 @@ func NewCollectionsCmd() *cobra.Command {
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if csConfig.Hub == nil {
 			if csConfig.Hub == nil {
 				return fmt.Errorf("you must configure cli before interacting with hub")
 				return fmt.Errorf("you must configure cli before interacting with hub")
@@ -32,8 +32,8 @@ func NewCollectionsCmd() *cobra.Command {
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 
 
 			return nil
 			return nil
@@ -60,11 +60,10 @@ func NewCollectionsCmd() *cobra.Command {
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			for _, name := range args {
 			for _, name := range args {
 				if err := cwhub.InstallItem(csConfig, name, cwhub.COLLECTIONS, forceAction, downloadOnly); err != nil {
 				if err := cwhub.InstallItem(csConfig, name, cwhub.COLLECTIONS, forceAction, downloadOnly); err != nil {
-					if ignoreError {
-						log.Errorf("Error while installing '%s': %s", name, err)
-					} else {
+					if !ignoreError {
 						log.Fatalf("Error while installing '%s': %s", name, err)
 						log.Fatalf("Error while installing '%s': %s", name, err)
 					}
 					}
+					log.Errorf("Error while installing '%s': %s", name, err)
 				}
 				}
 			}
 			}
 		},
 		},
@@ -91,7 +90,7 @@ func NewCollectionsCmd() *cobra.Command {
 			}
 			}
 
 
 			if len(args) == 0 {
 			if len(args) == 0 {
-				log.Fatalf("Specify at least one collection to remove or '--all' flag.")
+				log.Fatal("Specify at least one collection to remove or '--all' flag.")
 			}
 			}
 
 
 			for _, name := range args {
 			for _, name := range args {

+ 6 - 6
cmd/crowdsec-cli/config.go

@@ -47,13 +47,13 @@ func backupConfigToDirectory(dirPath string) error {
 	}
 	}
 
 
 	if err = os.Mkdir(dirPath, 0700); err != nil {
 	if err = os.Mkdir(dirPath, 0700); err != nil {
-		return fmt.Errorf("error while creating %s : %s", dirPath, err)
+		return errors.Wrapf(err, "while creating %s", dirPath)
 	}
 	}
 
 
 	if csConfig.ConfigPaths.SimulationFilePath != "" {
 	if csConfig.ConfigPaths.SimulationFilePath != "" {
 		backupSimulation := filepath.Join(dirPath, "simulation.yaml")
 		backupSimulation := filepath.Join(dirPath, "simulation.yaml")
 		if err = types.CopyFile(csConfig.ConfigPaths.SimulationFilePath, backupSimulation); err != nil {
 		if err = types.CopyFile(csConfig.ConfigPaths.SimulationFilePath, backupSimulation); err != nil {
-			return fmt.Errorf("failed copy %s to %s : %s", csConfig.ConfigPaths.SimulationFilePath, backupSimulation, err)
+			return errors.Wrapf(err, "failed copy %s to %s", csConfig.ConfigPaths.SimulationFilePath, backupSimulation)
 		}
 		}
 		log.Infof("Saved simulation to %s", backupSimulation)
 		log.Infof("Saved simulation to %s", backupSimulation)
 	}
 	}
@@ -437,11 +437,11 @@ func NewConfigCmd() *cobra.Command {
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			var err error
 			var err error
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err = cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err = cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			if err = backupConfigToDirectory(args[0]); err != nil {
 			if err = backupConfigToDirectory(args[0]); err != nil {
 				log.Fatalf("Failed to backup configurations: %s", err)
 				log.Fatalf("Failed to backup configurations: %s", err)
@@ -466,11 +466,11 @@ func NewConfigCmd() *cobra.Command {
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			var err error
 			var err error
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err = cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err = cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			if err := restoreConfigFromDirectory(args[0]); err != nil {
 			if err := restoreConfigFromDirectory(args[0]); err != nil {
 				log.Fatalf("failed restoring configurations from %s : %s", args[0], err)
 				log.Fatalf("failed restoring configurations from %s : %s", args[0], err)

+ 0 - 5
cmd/crowdsec-cli/configfile.go

@@ -1,5 +0,0 @@
-// +build linux freebsd netbsd openbsd solaris !windows
-
-package main
-
-const DefaultConfigFile = "/etc/crowdsec/config.yaml"

+ 0 - 6
cmd/crowdsec-cli/configfile_windows.go

@@ -1,6 +0,0 @@
-//go:build windows
-// +build windows
-
-package main
-
-const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml"

+ 2 - 2
cmd/crowdsec-cli/console.go

@@ -78,12 +78,12 @@ After running this command your will need to validate the enrollment in the weba
 			}
 			}
 
 
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 				log.Fatalf("Failed to load hub index : %s", err)
 				log.Fatalf("Failed to load hub index : %s", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 
 
 			scenarios, err := cwhub.GetInstalledScenariosAsString()
 			scenarios, err := cwhub.GetInstalledScenariosAsString()

+ 5 - 5
cmd/crowdsec-cli/hub.go

@@ -44,11 +44,11 @@ cscli hub update # Download list of available configurations from the hub
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 
 
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			//use LocalSync to get warnings about tainted / outdated items
 			//use LocalSync to get warnings about tainted / outdated items
 			_, warn := cwhub.LocalSync(csConfig.Hub)
 			_, warn := cwhub.LocalSync(csConfig.Hub)
@@ -84,7 +84,7 @@ Fetches the [.index.json](https://github.com/crowdsecurity/hub/blob/master/.inde
 		},
 		},
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err := cwhub.UpdateHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.UpdateHubIdx(csConfig.Hub); err != nil {
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
@@ -118,11 +118,11 @@ Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if
 		},
 		},
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 
 
 			log.Infof("Upgrading collections")
 			log.Infof("Upgrading collections")

+ 2 - 2
cmd/crowdsec-cli/lapi.go

@@ -134,12 +134,12 @@ Keep in mind the machine needs to be validated by an administrator on LAPI side
 				log.Fatalf("parsing api url ('%s'): %s", apiurl, err)
 				log.Fatalf("parsing api url ('%s'): %s", apiurl, err)
 			}
 			}
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to load hub index : %s", err)
 				log.Fatalf("Failed to load hub index : %s", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			scenarios, err := cwhub.GetInstalledScenariosAsString()
 			scenarios, err := cwhub.GetInstalledScenariosAsString()
 			if err != nil {
 			if err != nil {

+ 1 - 1
cmd/crowdsec-cli/main.go

@@ -155,7 +155,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
 
 
 	rootCmd.PersistentFlags().StringVar(&cwhub.HubBranch, "branch", "", "Override hub branch on github")
 	rootCmd.PersistentFlags().StringVar(&cwhub.HubBranch, "branch", "", "Override hub branch on github")
 	if err := rootCmd.PersistentFlags().MarkHidden("branch"); err != nil {
 	if err := rootCmd.PersistentFlags().MarkHidden("branch"); err != nil {
-		log.Fatalf("failed to make branch hidden : %s", err)
+		log.Fatalf("failed to hide flag: %s", err)
 	}
 	}
 
 
 	if len(os.Args) > 1 && os.Args[1] != "completion" && os.Args[1] != "version" && os.Args[1] != "help" {
 	if len(os.Args) > 1 && os.Args[1] != "completion" && os.Args[1] != "version" && os.Args[1] != "help" {

+ 7 - 12
cmd/crowdsec-cli/messages.go

@@ -5,24 +5,19 @@ import (
 	"runtime"
 	"runtime"
 )
 )
 
 
-const (
-	ReloadMessageFormat = `Run '%s' for the new configuration to be effective.`
-	ReloadCmdLinux      = `sudo systemctl reload crowdsec`
-	ReloadCmdFreebsd    = `sudo service crowdsec reload`
-)
-
+// ReloadMessage returns a description of the task required to reload
+// the crowdsec configuration, according to the operating system.
 func ReloadMessage() string {
 func ReloadMessage() string {
-
-	var reloadCmd string
+	var msg string
 
 
 	switch runtime.GOOS {
 	switch runtime.GOOS {
 	case "windows":
 	case "windows":
-		return "Please restart the crowdsec service for the new configuration to be effective."
+		msg = "Please restart the crowdsec service"
 	case "freebsd":
 	case "freebsd":
-		reloadCmd = ReloadCmdFreebsd
+		msg = `Run 'sudo service crowdsec reload'`
 	default:
 	default:
-		reloadCmd = ReloadCmdLinux
+		msg = `Run 'sudo systemctl reload crowdsec'`
 	}
 	}
 
 
-	return fmt.Sprintf(ReloadMessageFormat, reloadCmd)
+	return fmt.Sprintf("%s for the new configuration to be effective.", msg)
 }
 }

+ 2 - 2
cmd/crowdsec-cli/parsers.go

@@ -25,7 +25,7 @@ cscli parsers remove crowdsecurity/sshd-logs
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if csConfig.Hub == nil {
 			if csConfig.Hub == nil {
 				return fmt.Errorf("you must configure cli before interacting with hub")
 				return fmt.Errorf("you must configure cli before interacting with hub")
@@ -36,8 +36,8 @@ cscli parsers remove crowdsecurity/sshd-logs
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			return nil
 			return nil
 		},
 		},

+ 2 - 2
cmd/crowdsec-cli/postoverflows.go

@@ -24,7 +24,7 @@ func NewPostOverflowsCmd() *cobra.Command {
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if csConfig.Hub == nil {
 			if csConfig.Hub == nil {
 				return fmt.Errorf("you must configure cli before interacting with hub")
 				return fmt.Errorf("you must configure cli before interacting with hub")
@@ -35,8 +35,8 @@ func NewPostOverflowsCmd() *cobra.Command {
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 			return nil
 			return nil
 		},
 		},

+ 2 - 2
cmd/crowdsec-cli/scenarios.go

@@ -27,7 +27,7 @@ cscli scenarios remove crowdsecurity/ssh-bf
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if csConfig.Hub == nil {
 			if csConfig.Hub == nil {
 				return fmt.Errorf("you must configure cli before interacting with hub")
 				return fmt.Errorf("you must configure cli before interacting with hub")
@@ -38,7 +38,7 @@ cscli scenarios remove crowdsecurity/ssh-bf
 			}
 			}
 
 
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
 			}
 			}
 
 

+ 8 - 8
cmd/crowdsec-cli/simulation.go

@@ -134,11 +134,11 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			if err := csConfig.LoadHub(); err != nil {
 			if err := csConfig.LoadHub(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
 			if err := cwhub.GetHubIdx(csConfig.Hub); err != nil {
+				log.Info("Run 'sudo cscli hub update' to get the hub index")
 				log.Fatalf("Failed to get Hub index : %v", err)
 				log.Fatalf("Failed to get Hub index : %v", err)
-				log.Infoln("Run 'sudo cscli hub update' to get the hub index")
 			}
 			}
 
 
 			if len(args) > 0 {
 			if len(args) > 0 {
@@ -153,7 +153,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 					}
 					}
 					isExcluded := inSlice(scenario, csConfig.Cscli.SimulationConfig.Exclusions)
 					isExcluded := inSlice(scenario, csConfig.Cscli.SimulationConfig.Exclusions)
 					if *csConfig.Cscli.SimulationConfig.Simulation && !isExcluded {
 					if *csConfig.Cscli.SimulationConfig.Simulation && !isExcluded {
-						log.Warningf("global simulation is already enabled")
+						log.Warning("global simulation is already enabled")
 						continue
 						continue
 					}
 					}
 					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
 					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
@@ -162,13 +162,13 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 					}
 					}
 					if *csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
 					if *csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
 						if err := removeFromExclusion(scenario); err != nil {
 						if err := removeFromExclusion(scenario); err != nil {
-							log.Fatalf(err.Error())
+							log.Fatal(err)
 						}
 						}
 						log.Printf("simulation enabled for '%s'", scenario)
 						log.Printf("simulation enabled for '%s'", scenario)
 						continue
 						continue
 					}
 					}
 					if err := addToExclusion(scenario); err != nil {
 					if err := addToExclusion(scenario); err != nil {
-						log.Fatalf(err.Error())
+						log.Fatal(err)
 					}
 					}
 					log.Printf("simulation mode for '%s' enabled", scenario)
 					log.Printf("simulation mode for '%s' enabled", scenario)
 				}
 				}
@@ -202,7 +202,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 					}
 					}
 					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
 					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
 						if err := removeFromExclusion(scenario); err != nil {
 						if err := removeFromExclusion(scenario); err != nil {
-							log.Fatalf(err.Error())
+							log.Fatal(err)
 						}
 						}
 						log.Printf("simulation mode for '%s' disabled", scenario)
 						log.Printf("simulation mode for '%s' disabled", scenario)
 						continue
 						continue
@@ -212,7 +212,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 						continue
 						continue
 					}
 					}
 					if err := addToExclusion(scenario); err != nil {
 					if err := addToExclusion(scenario); err != nil {
-						log.Fatalf(err.Error())
+						log.Fatal(err)
 					}
 					}
 					log.Printf("simulation mode for '%s' disabled", scenario)
 					log.Printf("simulation mode for '%s' disabled", scenario)
 				}
 				}
@@ -238,7 +238,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		Run: func(cmd *cobra.Command, args []string) {
 		Run: func(cmd *cobra.Command, args []string) {
 			if err := simulationStatus(); err != nil {
 			if err := simulationStatus(); err != nil {
-				log.Fatalf(err.Error())
+				log.Fatal(err)
 			}
 			}
 		},
 		},
 		PersistentPostRun: func(cmd *cobra.Command, args []string) {
 		PersistentPostRun: func(cmd *cobra.Command, args []string) {

+ 1 - 1
cmd/crowdsec-cli/utils.go

@@ -51,7 +51,7 @@ func indexOf(s string, slice []string) int {
 
 
 func LoadHub() error {
 func LoadHub() error {
 	if err := csConfig.LoadHub(); err != nil {
 	if err := csConfig.LoadHub(); err != nil {
-		log.Fatalf(err.Error())
+		log.Fatal(err)
 	}
 	}
 	if csConfig.Hub == nil {
 	if csConfig.Hub == nil {
 		return fmt.Errorf("unable to load hub")
 		return fmt.Errorf("unable to load hub")

+ 4 - 5
cmd/crowdsec/api.go

@@ -1,7 +1,6 @@
 package main
 package main
 
 
 import (
 import (
-	"fmt"
 	"runtime"
 	"runtime"
 
 
 	"github.com/crowdsecurity/crowdsec/pkg/apiserver"
 	"github.com/crowdsecurity/crowdsec/pkg/apiserver"
@@ -21,17 +20,17 @@ func initAPIServer(cConfig *csconfig.Config) (*apiserver.APIServer, error) {
 		log.Info("initiating plugin broker")
 		log.Info("initiating plugin broker")
 		//On windows, the plugins are always run as medium-integrity processes, so we don't care about plugin_config
 		//On windows, the plugins are always run as medium-integrity processes, so we don't care about plugin_config
 		if cConfig.PluginConfig == nil && runtime.GOOS != "windows" {
 		if cConfig.PluginConfig == nil && runtime.GOOS != "windows" {
-			return nil, fmt.Errorf("plugins are enabled, but the plugin_config section is missing in the configuration")
+			return nil, errors.New("plugins are enabled, but the plugin_config section is missing in the configuration")
 		}
 		}
 		if cConfig.ConfigPaths.NotificationDir == "" {
 		if cConfig.ConfigPaths.NotificationDir == "" {
-			return nil, fmt.Errorf("plugins are enabled, but config_paths.notification_dir is not defined")
+			return nil, errors.New("plugins are enabled, but config_paths.notification_dir is not defined")
 		}
 		}
 		if cConfig.ConfigPaths.PluginDir == "" {
 		if cConfig.ConfigPaths.PluginDir == "" {
-			return nil, fmt.Errorf("plugins are enabled, but config_paths.plugin_dir is not defined")
+			return nil, errors.New("plugins are enabled, but config_paths.plugin_dir is not defined")
 		}
 		}
 		err = pluginBroker.Init(cConfig.PluginConfig, cConfig.API.Server.Profiles, cConfig.ConfigPaths)
 		err = pluginBroker.Init(cConfig.PluginConfig, cConfig.API.Server.Profiles, cConfig.ConfigPaths)
 		if err != nil {
 		if err != nil {
-			return nil, fmt.Errorf("unable to run local API: %s", err)
+			return nil, errors.Wrap(err, "unable to run local API")
 		}
 		}
 		log.Info("initiated plugin broker")
 		log.Info("initiated plugin broker")
 		apiServer.AttachPluginBroker(&pluginBroker)
 		apiServer.AttachPluginBroker(&pluginBroker)

+ 0 - 5
cmd/crowdsec/configfile.go

@@ -1,5 +0,0 @@
-// +build linux	freebsd netbsd openbsd solaris !windows
-
-package main
-
-const DefaultConfigFile = "/etc/crowdsec/config.yaml"

+ 0 - 3
cmd/crowdsec/configfile_windows.go

@@ -1,3 +0,0 @@
-package main
-
-const DefaultConfigFile = "C:\\ProgramData\\CrowdSec\\config\\config.yaml"

+ 40 - 6
cmd/crowdsec/main.go

@@ -10,6 +10,7 @@ import (
 	_ "net/http/pprof"
 	_ "net/http/pprof"
 	"time"
 	"time"
 
 
+	"github.com/confluentinc/bincover"
 	"github.com/crowdsecurity/crowdsec/pkg/acquisition"
 	"github.com/crowdsecurity/crowdsec/pkg/acquisition"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csplugin"
 	"github.com/crowdsecurity/crowdsec/pkg/csplugin"
@@ -196,7 +197,6 @@ func (f *Flags) Parse() {
 
 
 // LoadConfig returns a configuration parsed from configuration file
 // LoadConfig returns a configuration parsed from configuration file
 func LoadConfig(cConfig *csconfig.Config) error {
 func LoadConfig(cConfig *csconfig.Config) error {
-
 	if dumpFolder != "" {
 	if dumpFolder != "" {
 		parser.ParseDump = true
 		parser.ParseDump = true
 		parser.DumpFolder = dumpFolder
 		parser.DumpFolder = dumpFolder
@@ -217,11 +217,11 @@ func LoadConfig(cConfig *csconfig.Config) error {
 	}
 	}
 
 
 	if !cConfig.DisableAgent && (cConfig.API == nil || cConfig.API.Client == nil || cConfig.API.Client.Credentials == nil) {
 	if !cConfig.DisableAgent && (cConfig.API == nil || cConfig.API.Client == nil || cConfig.API.Client.Credentials == nil) {
-		log.Fatalf("missing local API credentials for crowdsec agent, abort")
+		return errors.New("missing local API credentials for crowdsec agent, abort")
 	}
 	}
 
 
 	if cConfig.DisableAPI && cConfig.DisableAgent {
 	if cConfig.DisableAPI && cConfig.DisableAgent {
-		log.Fatalf("You must run at least the API Server or crowdsec")
+		return errors.New("You must run at least the API Server or crowdsec")
 	}
 	}
 
 
 	if flags.DebugLevel {
 	if flags.DebugLevel {
@@ -260,8 +260,26 @@ func LoadConfig(cConfig *csconfig.Config) error {
 	return nil
 	return nil
 }
 }
 
 
-func main() {
+// This must be called right before the program termination, to allow
+// measuring functional test coverage in case of abnormal exit.
+//
+// without bincover: log error and exit with code
+// with bincover: log error and tell bincover the exit code, then return
+func exitWithCode(exitCode int, err error) {
+	if err != nil {
+		// this method of logging a fatal error does not
+		// trigger a program exit (as stated by the authors, it
+		// is not going to change in logrus to keep backward
+		// compatibility), and allows us to report coverage.
+		log.NewEntry(log.StandardLogger()).Log(log.FatalLevel, err)
+	}
+	if bincoverTesting == "" {
+		os.Exit(exitCode)
+	}
+	bincover.ExitCode = exitCode
+}
 
 
+func main() {
 	defer types.CatchPanic("crowdsec/main")
 	defer types.CatchPanic("crowdsec/main")
 
 
 	log.Debugf("os.Args: %v", os.Args)
 	log.Debugf("os.Args: %v", os.Args)
@@ -269,9 +287,25 @@ func main() {
 	// Handle command line arguments
 	// Handle command line arguments
 	flags = &Flags{}
 	flags = &Flags{}
 	flags.Parse()
 	flags.Parse()
+
+	if len(flag.Args()) > 0 {
+		fmt.Fprintf(os.Stderr, "argument provided but not defined: %s\n", flag.Args()[0])
+		flag.Usage()
+		// the flag package exits with 2 in case of unknown flag
+		exitWithCode(2, nil)
+		return
+	}
+
 	if flags.PrintVersion {
 	if flags.PrintVersion {
 		cwversion.Show()
 		cwversion.Show()
-		os.Exit(0)
+		exitWithCode(0, nil)
+		return
+	}
+
+	exitCode := 0
+	err := StartRunSvc()
+	if err != nil {
+		exitCode = 1
 	}
 	}
-	StartRunSvc()
+	exitWithCode(exitCode, err)
 }
 }

+ 4 - 18
cmd/crowdsec/run_in_svc.go

@@ -6,7 +6,6 @@ package main
 import (
 import (
 	"os"
 	"os"
 
 
-	"github.com/confluentinc/bincover"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
 	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
@@ -14,7 +13,7 @@ import (
 	"github.com/sirupsen/logrus/hooks/writer"
 	"github.com/sirupsen/logrus/hooks/writer"
 )
 )
 
 
-func StartRunSvc() {
+func StartRunSvc() error {
 	var (
 	var (
 		cConfig *csconfig.Config
 		cConfig *csconfig.Config
 		err     error
 		err     error
@@ -30,10 +29,10 @@ func StartRunSvc() {
 
 
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	if err != nil {
 	if err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 	if err := LoadConfig(cConfig); err != nil {
 	if err := LoadConfig(cConfig); err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 	// Configure logging
 	// Configure logging
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
@@ -51,18 +50,5 @@ func StartRunSvc() {
 	if cConfig.Prometheus != nil {
 	if cConfig.Prometheus != nil {
 		go registerPrometheus(cConfig.Prometheus)
 		go registerPrometheus(cConfig.Prometheus)
 	}
 	}
-
-	if exitCode, err := Serve(cConfig); err != nil {
-		if err != nil {
-			// this method of logging a fatal error does not
-			// trigger a program exit (as stated by the authors, it
-			// is not going to change in logrus to keep backward
-			// compatibility), and allows us to report coverage.
-			log.NewEntry(log.StandardLogger()).Log(log.FatalLevel, err)
-			if bincoverTesting == "" {
-				os.Exit(exitCode)
-			}
-			bincover.ExitCode = exitCode
-		}
-	}
+	return Serve(cConfig)
 }
 }

+ 17 - 32
cmd/crowdsec/run_in_svc_windows.go

@@ -1,61 +1,59 @@
 package main
 package main
 
 
 import (
 import (
+	"fmt"
 	"os"
 	"os"
 
 
-	"github.com/confluentinc/bincover"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
 	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"github.com/sirupsen/logrus/hooks/writer"
 	"github.com/sirupsen/logrus/hooks/writer"
 	"golang.org/x/sys/windows/svc"
 	"golang.org/x/sys/windows/svc"
 )
 )
 
 
-func StartRunSvc() {
-
+func StartRunSvc() error {
 	const svcName = "CrowdSec"
 	const svcName = "CrowdSec"
 	const svcDescription = "Crowdsec IPS/IDS"
 	const svcDescription = "Crowdsec IPS/IDS"
 
 
 	isRunninginService, err := svc.IsWindowsService()
 	isRunninginService, err := svc.IsWindowsService()
 	if err != nil {
 	if err != nil {
-		log.Fatalf("failed to determine if we are running in windows service mode: %v", err)
+		return errors.Wrap(err, "failed to determine if we are running in windows service mode")
 	}
 	}
 	if isRunninginService {
 	if isRunninginService {
-		runService(svcName)
-		return
+		return runService(svcName)
 	}
 	}
 
 
 	if flags.WinSvc == "Install" {
 	if flags.WinSvc == "Install" {
 		err = installService(svcName, svcDescription)
 		err = installService(svcName, svcDescription)
 		if err != nil {
 		if err != nil {
-			log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err)
+			return errors.Wrapf(err, "failed to %s %s", flags.WinSvc, svcName)
 		}
 		}
 	} else if flags.WinSvc == "Remove" {
 	} else if flags.WinSvc == "Remove" {
 		err = removeService(svcName)
 		err = removeService(svcName)
 		if err != nil {
 		if err != nil {
-			log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err)
+			return errors.Wrapf(err, "failed to %s %s", flags.WinSvc, svcName)
 		}
 		}
 	} else if flags.WinSvc == "Start" {
 	} else if flags.WinSvc == "Start" {
 		err = startService(svcName)
 		err = startService(svcName)
 		if err != nil {
 		if err != nil {
-			log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err)
+			return errors.Wrapf(err, "failed to %s %s", flags.WinSvc, svcName)
 		}
 		}
 	} else if flags.WinSvc == "Stop" {
 	} else if flags.WinSvc == "Stop" {
 		err = controlService(svcName, svc.Stop, svc.Stopped)
 		err = controlService(svcName, svc.Stop, svc.Stopped)
 		if err != nil {
 		if err != nil {
-			log.Fatalf("failed to %s %s: %v", flags.WinSvc, svcName, err)
+			return errors.Wrapf(err, "failed to %s %s", flags.WinSvc, svcName)
 		}
 		}
 	} else if flags.WinSvc == "" {
 	} else if flags.WinSvc == "" {
-		WindowsRun()
+		return WindowsRun()
 	} else {
 	} else {
-		log.Fatalf("Invalid value for winsvc parameter: %s", flags.WinSvc)
+		return fmt.Errorf("Invalid value for winsvc parameter: %s", flags.WinSvc)
 	}
 	}
-
+	return nil
 }
 }
 
 
-func WindowsRun() {
-
+func WindowsRun() error {
 	var (
 	var (
 		cConfig *csconfig.Config
 		cConfig *csconfig.Config
 		err     error
 		err     error
@@ -71,15 +69,15 @@ func WindowsRun() {
 
 
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	if err != nil {
 	if err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 	if err := LoadConfig(cConfig); err != nil {
 	if err := LoadConfig(cConfig); err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 	// Configure logging
 	// Configure logging
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
-		log.Fatal(err.Error())
+		return err
 	}
 	}
 
 
 	log.Infof("Crowdsec %s", cwversion.VersionStr())
 	log.Infof("Crowdsec %s", cwversion.VersionStr())
@@ -92,18 +90,5 @@ func WindowsRun() {
 	if cConfig.Prometheus != nil {
 	if cConfig.Prometheus != nil {
 		go registerPrometheus(cConfig.Prometheus)
 		go registerPrometheus(cConfig.Prometheus)
 	}
 	}
-
-	if exitCode, err := Serve(cConfig); err != nil {
-		if err != nil {
-			// this method of logging a fatal error does not
-			// trigger a program exit (as stated by the authors, it
-			// is not going to change in logrus to keep backward
-			// compatibility), and allows us to report coverage.
-			log.NewEntry(log.StandardLogger()).Log(log.FatalLevel, err)
-			if bincoverTesting != "" {
-				os.Exit(exitCode)
-			}
-			bincover.ExitCode = exitCode
-		}
-	}
+	return Serve(cConfig)
 }
 }

+ 27 - 24
cmd/crowdsec/serve.go

@@ -1,7 +1,6 @@
 package main
 package main
 
 
 import (
 import (
-	"fmt"
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
 	"syscall"
 	"syscall"
@@ -18,7 +17,7 @@ import (
 	//"github.com/sevlyar/go-daemon"
 	//"github.com/sevlyar/go-daemon"
 )
 )
 
 
-// debugHandler is kept as a dev convenience : it shuts down and serialize internal state
+//nolint: deadcode,unused // debugHandler is kept as a dev convenience : it shuts down and serialize internal state
 func debugHandler(sig os.Signal, cConfig *csconfig.Config) error {
 func debugHandler(sig os.Signal, cConfig *csconfig.Config) error {
 	var tmpFile string
 	var tmpFile string
 	var err error
 	var err error
@@ -53,22 +52,22 @@ func reloadHandler(sig os.Signal, cConfig *csconfig.Config) error {
 
 
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	cConfig, err = csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	if err != nil {
 	if err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 
 
 	if err := LoadConfig(cConfig); err != nil {
 	if err := LoadConfig(cConfig); err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
 	// Configure logging
 	// Configure logging
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
-		log.Fatal(err.Error())
+		return err
 	}
 	}
 
 
 	if !cConfig.DisableAPI {
 	if !cConfig.DisableAPI {
 		apiServer, err := initAPIServer(cConfig)
 		apiServer, err := initAPIServer(cConfig)
 		if err != nil {
 		if err != nil {
-			return fmt.Errorf("unable to init api server: %s", err)
+			return errors.Wrap(err, "unable to init api server")
 		}
 		}
 
 
 		serveAPIServer(apiServer)
 		serveAPIServer(apiServer)
@@ -77,7 +76,7 @@ func reloadHandler(sig os.Signal, cConfig *csconfig.Config) error {
 	if !cConfig.DisableAgent {
 	if !cConfig.DisableAgent {
 		csParsers, err := initCrowdsec(cConfig)
 		csParsers, err := initCrowdsec(cConfig)
 		if err != nil {
 		if err != nil {
-			return fmt.Errorf("unable to init crowdsec: %s", err)
+			return errors.Wrap(err, "unable to init crowdsec")
 		}
 		}
 		//restore bucket state
 		//restore bucket state
 		if tmpFile != "" {
 		if tmpFile != "" {
@@ -164,20 +163,18 @@ func shutdownCrowdsec() error {
 func shutdown(sig os.Signal, cConfig *csconfig.Config) error {
 func shutdown(sig os.Signal, cConfig *csconfig.Config) error {
 	if !cConfig.DisableAgent {
 	if !cConfig.DisableAgent {
 		if err := shutdownCrowdsec(); err != nil {
 		if err := shutdownCrowdsec(); err != nil {
-			log.Errorf("Failed to shut down crowdsec: %s", err)
-			return err
+			return errors.Wrap(err, "Failed to shut down crowdsec")
 		}
 		}
 	}
 	}
 	if !cConfig.DisableAPI {
 	if !cConfig.DisableAPI {
 		if err := shutdownAPI(); err != nil {
 		if err := shutdownAPI(); err != nil {
-			log.Errorf("Failed to shut down api routines: %s", err)
-			return err
+			return errors.Wrap(err, "Failed to shut down api routines")
 		}
 		}
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func HandleSignals(cConfig *csconfig.Config) int {
+func HandleSignals(cConfig *csconfig.Config) error {
 	signalChan := make(chan os.Signal, 1)
 	signalChan := make(chan os.Signal, 1)
 	//We add os.Interrupt mostly to ease windows dev, it allows to simulate a clean shutdown when running in the console
 	//We add os.Interrupt mostly to ease windows dev, it allows to simulate a clean shutdown when running in the console
 	signal.Notify(signalChan,
 	signal.Notify(signalChan,
@@ -185,9 +182,10 @@ func HandleSignals(cConfig *csconfig.Config) int {
 		syscall.SIGTERM,
 		syscall.SIGTERM,
 		os.Interrupt)
 		os.Interrupt)
 
 
-	exitChan := make(chan int)
+	exitChan := make(chan error)
 	go func() {
 	go func() {
 		defer types.CatchPanic("crowdsec/HandleSignals")
 		defer types.CatchPanic("crowdsec/HandleSignals")
+	Loop:
 		for {
 		for {
 			s := <-signalChan
 			s := <-signalChan
 			switch s {
 			switch s {
@@ -195,28 +193,33 @@ func HandleSignals(cConfig *csconfig.Config) int {
 			case syscall.SIGHUP:
 			case syscall.SIGHUP:
 				log.Warningf("SIGHUP received, reloading")
 				log.Warningf("SIGHUP received, reloading")
 				if err := shutdown(s, cConfig); err != nil {
 				if err := shutdown(s, cConfig); err != nil {
-					log.Fatalf("failed shutdown : %s", err)
+					exitChan <- errors.Wrap(err, "failed shutdown")
+					break Loop
 				}
 				}
 				if err := reloadHandler(s, cConfig); err != nil {
 				if err := reloadHandler(s, cConfig); err != nil {
-					log.Fatalf("Reload handler failure : %s", err)
+					exitChan <- errors.Wrap(err, "reload handler failure")
+					break Loop
 				}
 				}
 			// ctrl+C, kill -SIGINT XXXX, kill -SIGTERM XXXX
 			// ctrl+C, kill -SIGINT XXXX, kill -SIGTERM XXXX
 			case os.Interrupt, syscall.SIGTERM:
 			case os.Interrupt, syscall.SIGTERM:
 				log.Warningf("SIGTERM received, shutting down")
 				log.Warningf("SIGTERM received, shutting down")
 				if err := shutdown(s, cConfig); err != nil {
 				if err := shutdown(s, cConfig); err != nil {
-					log.Fatalf("failed shutdown : %s", err)
+					exitChan <- errors.Wrap(err, "failed shutdown")
+					break Loop
 				}
 				}
-				exitChan <- 0
+				exitChan <- nil
 			}
 			}
 		}
 		}
 	}()
 	}()
 
 
-	code := <-exitChan
-	log.Warningf("Crowdsec service shutting down")
-	return code
+	err := <-exitChan
+	if err == nil {
+		log.Warningf("Crowdsec service shutting down")
+	}
+	return err
 }
 }
 
 
-func Serve(cConfig *csconfig.Config) (int, error) {
+func Serve(cConfig *csconfig.Config) error {
 	acquisTomb = tomb.Tomb{}
 	acquisTomb = tomb.Tomb{}
 	parsersTomb = tomb.Tomb{}
 	parsersTomb = tomb.Tomb{}
 	bucketsTomb = tomb.Tomb{}
 	bucketsTomb = tomb.Tomb{}
@@ -227,7 +230,7 @@ func Serve(cConfig *csconfig.Config) (int, error) {
 	if !cConfig.DisableAPI {
 	if !cConfig.DisableAPI {
 		apiServer, err := initAPIServer(cConfig)
 		apiServer, err := initAPIServer(cConfig)
 		if err != nil {
 		if err != nil {
-			return 1, errors.Wrap(err, "api server init")
+			return errors.Wrap(err, "api server init")
 		}
 		}
 		if !flags.TestMode {
 		if !flags.TestMode {
 			serveAPIServer(apiServer)
 			serveAPIServer(apiServer)
@@ -237,7 +240,7 @@ func Serve(cConfig *csconfig.Config) (int, error) {
 	if !cConfig.DisableAgent {
 	if !cConfig.DisableAgent {
 		csParsers, err := initCrowdsec(cConfig)
 		csParsers, err := initCrowdsec(cConfig)
 		if err != nil {
 		if err != nil {
-			return 1, errors.Wrap(err, "crowdsec init")
+			return errors.Wrap(err, "crowdsec init")
 		}
 		}
 		/* if it's just linting, we're done */
 		/* if it's just linting, we're done */
 		if !flags.TestMode {
 		if !flags.TestMode {
@@ -256,7 +259,7 @@ func Serve(cConfig *csconfig.Config) (int, error) {
 			log.Errorf("Failed to notify(sent: %v): %v", sent, err)
 			log.Errorf("Failed to notify(sent: %v): %v", sent, err)
 		}
 		}
 		/*wait for signals*/
 		/*wait for signals*/
-		return HandleSignals(cConfig), nil
+		return HandleSignals(cConfig)
 	}
 	}
 
 
 	for {
 	for {

+ 13 - 10
cmd/crowdsec/win_service.go

@@ -12,6 +12,7 @@ import (
 
 
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
+	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/sys/windows/svc"
 	"golang.org/x/sys/windows/svc"
 )
 )
@@ -61,27 +62,29 @@ loop:
 	return
 	return
 }
 }
 
 
-func runService(name string) {
+func runService(name string) error {
 	cConfig, err := csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	cConfig, err := csconfig.NewConfig(flags.ConfigFile, flags.DisableAgent, flags.DisableAPI)
 	if err != nil {
 	if err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
+
 	if err := LoadConfig(cConfig); err != nil {
 	if err := LoadConfig(cConfig); err != nil {
-		log.Fatalf(err.Error())
+		return err
 	}
 	}
+
 	// Configure logging
 	// Configure logging
-	if err = types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
+	if err := types.SetDefaultLoggerConfig(cConfig.Common.LogMedia, cConfig.Common.LogDir, *cConfig.Common.LogLevel,
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
 		cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles, cConfig.Common.LogMaxAge, cConfig.Common.CompressLogs); err != nil {
-		log.Fatal(err.Error())
+		return err
 	}
 	}
-	log.Infof("starting %s service", name)
 
 
+	log.Infof("starting %s service", name)
 	winsvc := crowdsec_winservice{config: cConfig}
 	winsvc := crowdsec_winservice{config: cConfig}
 
 
-	err = svc.Run(name, &winsvc)
-	if err != nil {
-		log.Errorf("%s service failed: %s", name, err)
-		return
+	if err := svc.Run(name, &winsvc); err != nil {
+		return errors.Wrapf(err, "%s service failed", name)
 	}
 	}
+
 	log.Infof("%s service stopped", name)
 	log.Infof("%s service stopped", name)
+	return nil
 }
 }

+ 1 - 0
pkg/acquisition/acquisition_test.go

@@ -493,6 +493,7 @@ READLOOP:
 	}
 	}
 }
 }
 
 
+//nolint: structcheck,unused
 type MockSourceByDSN struct {
 type MockSourceByDSN struct {
 	configuration.DataSourceCommonCfg `yaml:",inline"`
 	configuration.DataSourceCommonCfg `yaml:",inline"`
 	Toto                              string `yaml:"toto"`
 	Toto                              string `yaml:"toto"`

+ 24 - 21
pkg/acquisition/modules/file/file.go

@@ -137,14 +137,14 @@ func (f *FileSource) ConfigureByDSN(dsn string, labels map[string]string, logger
 	if len(args) == 2 && len(args[1]) != 0 {
 	if len(args) == 2 && len(args[1]) != 0 {
 		params, err := url.ParseQuery(args[1])
 		params, err := url.ParseQuery(args[1])
 		if err != nil {
 		if err != nil {
-			return fmt.Errorf("could not parse file args : %s", err)
+			return errors.Wrap(err, "could not parse file args")
 		}
 		}
 		for key, value := range params {
 		for key, value := range params {
 			if key != "log_level" {
 			if key != "log_level" {
 				return fmt.Errorf("unsupported key %s in file DSN", key)
 				return fmt.Errorf("unsupported key %s in file DSN", key)
 			}
 			}
 			if len(value) != 1 {
 			if len(value) != 1 {
-				return fmt.Errorf("expected zero or one value for 'log_level'")
+				return errors.New("expected zero or one value for 'log_level'")
 			}
 			}
 			lvl, err := log.ParseLevel(value[0])
 			lvl, err := log.ParseLevel(value[0])
 			if err != nil {
 			if err != nil {
@@ -351,7 +351,6 @@ func (f *FileSource) tailFile(out chan types.Event, t *tomb.Tomb, tail *tail.Tai
 	logger := f.logger.WithField("tail", tail.Filename)
 	logger := f.logger.WithField("tail", tail.Filename)
 	logger.Debugf("-> Starting tail of %s", tail.Filename)
 	logger.Debugf("-> Starting tail of %s", tail.Filename)
 	for {
 	for {
-		l := types.Line{}
 		select {
 		select {
 		case <-t.Dying():
 		case <-t.Dying():
 			logger.Infof("File datasource %s stopping", tail.Filename)
 			logger.Infof("File datasource %s stopping", tail.Filename)
@@ -377,19 +376,22 @@ func (f *FileSource) tailFile(out chan types.Event, t *tomb.Tomb, tail *tail.Tai
 				continue
 				continue
 			}
 			}
 			linesRead.With(prometheus.Labels{"source": tail.Filename}).Inc()
 			linesRead.With(prometheus.Labels{"source": tail.Filename}).Inc()
-			l.Raw = trimLine(line.Text)
-			l.Labels = f.config.Labels
-			l.Time = line.Time
-			l.Src = tail.Filename
-			l.Process = true
-			l.Module = f.GetName()
+			l := types.Line{
+				Raw:     trimLine(line.Text),
+				Labels:  f.config.Labels,
+				Time:    line.Time,
+				Src:     tail.Filename,
+				Process: true,
+				Module:  f.GetName(),
+			}
 			//we're tailing, it must be real time logs
 			//we're tailing, it must be real time logs
 			logger.Debugf("pushing %+v", l)
 			logger.Debugf("pushing %+v", l)
-			if !f.config.UseTimeMachine {
-				out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.LIVE}
-			} else {
-				out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: leaky.TIMEMACHINE}
+
+			expectMode := leaky.LIVE
+			if f.config.UseTimeMachine {
+				expectMode = leaky.TIMEMACHINE
 			}
 			}
+			out <- types.Event{Line: l, Process: true, Type: types.LOG, ExpectMode: expectMode}
 		}
 		}
 	}
 	}
 }
 }
@@ -421,14 +423,15 @@ func (f *FileSource) readFile(filename string, out chan types.Event, t *tomb.Tom
 		if scanner.Text() == "" {
 		if scanner.Text() == "" {
 			continue
 			continue
 		}
 		}
-		logger.Debugf("line %s", scanner.Text())
-		l := types.Line{}
-		l.Raw = scanner.Text()
-		l.Time = time.Now().UTC()
-		l.Src = filename
-		l.Labels = f.config.Labels
-		l.Process = true
-		l.Module = f.GetName()
+		l := types.Line{
+			Raw:     scanner.Text(),
+			Time:    time.Now().UTC(),
+			Src:     filename,
+			Labels:  f.config.Labels,
+			Process: true,
+			Module:  f.GetName(),
+		}
+		logger.Debugf("line %s", l.Raw)
 		linesRead.With(prometheus.Labels{"source": filename}).Inc()
 		linesRead.With(prometheus.Labels{"source": filename}).Inc()
 
 
 		//we're reading logs at once, it must be time-machine buckets
 		//we're reading logs at once, it must be time-machine buckets

+ 4 - 4
pkg/apiclient/alerts_service_test.go

@@ -37,7 +37,7 @@ func TestAlertsListAsMachine(t *testing.T) {
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	defer teardown()
 	defer teardown()
@@ -240,7 +240,7 @@ func TestAlertsGetAsMachine(t *testing.T) {
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	defer teardown()
 	defer teardown()
@@ -431,7 +431,7 @@ func TestAlertsCreateAsMachine(t *testing.T) {
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	defer teardown()
 	defer teardown()
@@ -475,7 +475,7 @@ func TestAlertsDeleteAsMachine(t *testing.T) {
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	defer teardown()
 	defer teardown()

+ 4 - 4
pkg/apiclient/auth_service_test.go

@@ -57,7 +57,7 @@ func TestWatcherAuth(t *testing.T) {
 	client, err := NewClient(mycfg)
 	client, err := NewClient(mycfg)
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
 	_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
@@ -81,7 +81,7 @@ func TestWatcherAuth(t *testing.T) {
 	client, err = NewClient(mycfg)
 	client, err = NewClient(mycfg)
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
 	_, err = client.Auth.AuthenticateWatcher(context.Background(), models.WatcherAuthRequest{
@@ -171,7 +171,7 @@ func TestWatcherUnregister(t *testing.T) {
 	client, err := NewClient(mycfg)
 	client, err := NewClient(mycfg)
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 	_, err = client.Auth.UnregisterWatcher(context.Background())
 	_, err = client.Auth.UnregisterWatcher(context.Background())
 	if err != nil {
 	if err != nil {
@@ -225,7 +225,7 @@ func TestWatcherEnroll(t *testing.T) {
 	client, err := NewClient(mycfg)
 	client, err := NewClient(mycfg)
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	_, err = client.Auth.EnrollWatcher(context.Background(), "goodkey", "", []string{}, false)
 	_, err = client.Auth.EnrollWatcher(context.Background(), "goodkey", "", []string{}, false)

+ 1 - 1
pkg/apiclient/auth_test.go

@@ -41,7 +41,7 @@ func TestApiAuth(t *testing.T) {
 
 
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	alert := DecisionsListOpts{IPEquals: new(string)}
 	alert := DecisionsListOpts{IPEquals: new(string)}

+ 5 - 5
pkg/apiclient/decisions_service_test.go

@@ -46,7 +46,7 @@ func TestDecisionsList(t *testing.T) {
 
 
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	tduration := "3h59m55.756182786s"
 	tduration := "3h59m55.756182786s"
@@ -77,7 +77,7 @@ func TestDecisionsList(t *testing.T) {
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 	if !reflect.DeepEqual(*decisions, *expected) {
 	if !reflect.DeepEqual(*decisions, *expected) {
 		t.Fatalf("returned %+v, want %+v", resp, expected)
 		t.Fatalf("returned %+v, want %+v", resp, expected)
@@ -137,7 +137,7 @@ func TestDecisionsStream(t *testing.T) {
 
 
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client())
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	tduration := "3h59m55.756182786s"
 	tduration := "3h59m55.756182786s"
@@ -168,7 +168,7 @@ func TestDecisionsStream(t *testing.T) {
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 	if !reflect.DeepEqual(*decisions, *expected) {
 	if !reflect.DeepEqual(*decisions, *expected) {
 		t.Fatalf("returned %+v, want %+v", resp, expected)
 		t.Fatalf("returned %+v, want %+v", resp, expected)
@@ -220,7 +220,7 @@ func TestDeleteDecisions(t *testing.T) {
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		log.Fatalf("new api client: %s", err.Error())
+		log.Fatalf("new api client: %s", err)
 	}
 	}
 
 
 	filters := DecisionsDeleteOpts{IPEquals: new(string)}
 	filters := DecisionsDeleteOpts{IPEquals: new(string)}

+ 1 - 1
pkg/apiserver/api_key_test.go

@@ -18,7 +18,7 @@ func TestAPIKey(t *testing.T) {
 
 
 	APIKey, err := CreateTestBouncer(config.API.Server.DbConfig)
 	APIKey, err := CreateTestBouncer(config.API.Server.DbConfig)
 	if err != nil {
 	if err != nil {
-		log.Fatalf("%s", err.Error())
+		log.Fatal(err)
 	}
 	}
 	// Login with empty token
 	// Login with empty token
 	w := httptest.NewRecorder()
 	w := httptest.NewRecorder()

+ 2 - 2
pkg/apiserver/apiserver.go

@@ -276,11 +276,11 @@ func (s *APIServer) Run() error {
 		go func() {
 		go func() {
 			if s.TLS != nil && s.TLS.CertFilePath != "" && s.TLS.KeyFilePath != "" {
 			if s.TLS != nil && s.TLS.CertFilePath != "" && s.TLS.KeyFilePath != "" {
 				if err := s.httpServer.ListenAndServeTLS(s.TLS.CertFilePath, s.TLS.KeyFilePath); err != nil {
 				if err := s.httpServer.ListenAndServeTLS(s.TLS.CertFilePath, s.TLS.KeyFilePath); err != nil {
-					log.Fatalf(err.Error())
+					log.Fatal(err)
 				}
 				}
 			} else {
 			} else {
 				if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed {
 				if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed {
-					log.Fatalf(err.Error())
+					log.Fatal(err)
 				}
 				}
 			}
 			}
 		}()
 		}()

+ 4 - 4
pkg/apiserver/machines_test.go

@@ -38,7 +38,7 @@ func TestCreateMachine(t *testing.T) {
 	// Create machine
 	// Create machine
 	b, err := json.Marshal(MachineTest)
 	b, err := json.Marshal(MachineTest)
 	if err != nil {
 	if err != nil {
-		log.Fatalf("unable to marshal MachineTest")
+		log.Fatal("unable to marshal MachineTest")
 	}
 	}
 	body := string(b)
 	body := string(b)
 
 
@@ -61,7 +61,7 @@ func TestCreateMachineWithForwardedFor(t *testing.T) {
 	// Create machine
 	// Create machine
 	b, err := json.Marshal(MachineTest)
 	b, err := json.Marshal(MachineTest)
 	if err != nil {
 	if err != nil {
-		log.Fatalf("unable to marshal MachineTest")
+		log.Fatal("unable to marshal MachineTest")
 	}
 	}
 	body := string(b)
 	body := string(b)
 
 
@@ -90,7 +90,7 @@ func TestCreateMachineWithForwardedForNoConfig(t *testing.T) {
 	// Create machine
 	// Create machine
 	b, err := json.Marshal(MachineTest)
 	b, err := json.Marshal(MachineTest)
 	if err != nil {
 	if err != nil {
-		log.Fatalf("unable to marshal MachineTest")
+		log.Fatal("unable to marshal MachineTest")
 	}
 	}
 	body := string(b)
 	body := string(b)
 
 
@@ -121,7 +121,7 @@ func TestCreateMachineWithoutForwardedFor(t *testing.T) {
 	// Create machine
 	// Create machine
 	b, err := json.Marshal(MachineTest)
 	b, err := json.Marshal(MachineTest)
 	if err != nil {
 	if err != nil {
-		log.Fatalf("unable to marshal MachineTest")
+		log.Fatal("unable to marshal MachineTest")
 	}
 	}
 	body := string(b)
 	body := string(b)
 
 

+ 29 - 16
pkg/apiserver/middlewares/v1/jwt.go

@@ -138,29 +138,42 @@ func Unauthorized(c *gin.Context, code int, message string) {
 	})
 	})
 }
 }
 
 
+func randomSecret() ([]byte, error) {
+	size := 64
+	secret := make([]byte, size)
+
+	n, err := rand.Read(secret)
+	if err != nil {
+		return nil, errors.New("unable to generate a new random seed for JWT generation")
+	}
+
+	if n != size {
+		return nil, errors.New("not enough entropy at random seed generation for JWT generation")
+	}
+
+	return secret, nil
+}
+
 func NewJWT(dbClient *database.Client) (*JWT, error) {
 func NewJWT(dbClient *database.Client) (*JWT, error) {
 	// Get secret from environment variable "SECRET"
 	// Get secret from environment variable "SECRET"
 	var (
 	var (
 		secret []byte
 		secret []byte
+		err    error
 	)
 	)
 
 
-	//Please be aware that brute force HS256 is possible.
-	//PLEASE choose a STRONG secret
-	secret_string := os.Getenv("CS_LAPI_SECRET")
-	if secret_string == "" {
-		secret = make([]byte, 64)
-		if n, err := rand.Read(secret); err != nil {
-			log.Fatalf("unable to generate a new random seed for JWT generation")
-		} else {
-			if n != 64 {
-				log.Fatalf("not enough entropy at random seed generation for JWT generation")
-			}
-		}
-	} else {
-		secret = []byte(secret_string)
-		if len(secret) < 64 {
-			log.Fatalf("secret not strong enough")
+	// Please be aware that brute force HS256 is possible.
+	// PLEASE choose a STRONG secret
+	secretString := os.Getenv("CS_LAPI_SECRET")
+	secret = []byte(secretString)
+
+	switch l := len(secret); {
+	case l == 0:
+		secret, err = randomSecret()
+		if err != nil {
+			return &JWT{}, err
 		}
 		}
+	case l < 64:
+		return &JWT{}, errors.New("CS_LAPI_SECRET not strong enough")
 	}
 	}
 
 
 	jwtMiddleware := &JWT{
 	jwtMiddleware := &JWT{

+ 0 - 9
pkg/apiserver/testutils.go

@@ -1,9 +0,0 @@
-//go:build !windows
-
-package apiserver
-
-import "os"
-
-func cleanFile(path string) {
-	os.Remove(path)
-}

+ 0 - 7
pkg/apiserver/testutils_windows.go

@@ -1,7 +0,0 @@
-package apiserver
-
-import "os"
-
-func cleanFile(path string) {
-	os.Remove(path)
-}

+ 1 - 1
pkg/csconfig/crowdsec_service.go

@@ -114,7 +114,7 @@ func (c *Config) LoadCrowdsec() error {
 		return fmt.Errorf("loading api client: %s", err.Error())
 		return fmt.Errorf("loading api client: %s", err.Error())
 	}
 	}
 	if err := c.LoadHub(); err != nil {
 	if err := c.LoadHub(); err != nil {
-		return fmt.Errorf("loading hub: %s", err)
+		return errors.Wrap(err, "while loading hub")
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 4 - 4
pkg/csconfig/hub_test.go

@@ -12,22 +12,22 @@ import (
 func TestLoadHub(t *testing.T) {
 func TestLoadHub(t *testing.T) {
 	hubFullPath, err := filepath.Abs("./hub")
 	hubFullPath, err := filepath.Abs("./hub")
 	if err != nil {
 	if err != nil {
-		t.Fatalf(err.Error())
+		t.Fatal(err)
 	}
 	}
 
 
 	dataFullPath, err := filepath.Abs("./data")
 	dataFullPath, err := filepath.Abs("./data")
 	if err != nil {
 	if err != nil {
-		t.Fatalf(err.Error())
+		t.Fatal(err)
 	}
 	}
 
 
 	configDirFullPath, err := filepath.Abs("./tests")
 	configDirFullPath, err := filepath.Abs("./tests")
 	if err != nil {
 	if err != nil {
-		t.Fatalf(err.Error())
+		t.Fatal(err)
 	}
 	}
 
 
 	hubIndexFileFullPath, err := filepath.Abs("./hub/.index.json")
 	hubIndexFileFullPath, err := filepath.Abs("./hub/.index.json")
 	if err != nil {
 	if err != nil {
-		t.Fatalf(err.Error())
+		t.Fatal(err)
 	}
 	}
 
 
 	tests := []struct {
 	tests := []struct {

+ 0 - 1
pkg/cwhub/cwhub_test.go

@@ -133,7 +133,6 @@ func TestGetters(t *testing.T) {
 }
 }
 
 
 func TestIndexDownload(t *testing.T) {
 func TestIndexDownload(t *testing.T) {
-
 	cfg := test_prepenv()
 	cfg := test_prepenv()
 
 
 	err := UpdateHubIdx(cfg.Hub)
 	err := UpdateHubIdx(cfg.Hub)

+ 1 - 1
pkg/parser/parsing_test.go

@@ -295,7 +295,7 @@ func testSubSet(testSet TestFile, pctx UnixParserCtx, nodes []Node) (bool, error
 		only the keys of the expected part are checked against result
 		only the keys of the expected part are checked against result
 	*/
 	*/
 	if len(testSet.Results) == 0 && len(results) == 0 {
 	if len(testSet.Results) == 0 && len(results) == 0 {
-		log.Fatalf("No results, no tests, abort.")
+		log.Fatal("No results, no tests, abort.")
 		return false, fmt.Errorf("no tests, no results")
 		return false, fmt.Errorf("no tests, no results")
 	}
 	}
 
 

+ 1 - 1
pkg/parser/runtime.go

@@ -187,7 +187,7 @@ func (n *Node) ProcessStatics(statics []types.ExtraField, event *types.Event) er
 				clog.Debugf("%s = '%s'", static.TargetByName, value)
 				clog.Debugf("%s = '%s'", static.TargetByName, value)
 			}
 			}
 		} else {
 		} else {
-			clog.Fatalf("unable to process static : unknown tartget")
+			clog.Fatal("unable to process static : unknown target")
 		}
 		}
 
 
 	}
 	}

+ 37 - 4
tests/bats/01_base.bats

@@ -13,6 +13,7 @@ teardown_file() {
 
 
 setup() {
 setup() {
     load "../lib/setup.sh"
     load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
     ./instance-data load
     ./instance-data load
     ./instance-crowdsec start
     ./instance-crowdsec start
 }
 }
@@ -139,15 +140,45 @@ declare stderr
     assert_output "127.0.0.1:8080"
     assert_output "127.0.0.1:8080"
 }
 }
 
 
-@test "${FILE} cscli config backup" {
+@test "${FILE} cscli config backup / restore" {
+    # test that we need a valid path
+    # disabled because in CI, the empty string is not passed as a parameter
+    ## run -1 --separate-stderr cscli config backup ""
+    ## run -0 echo "${stderr}"
+    ## assert_output --partial "Failed to backup configurations: directory path can't be empty"
+
+    run -1 --separate-stderr cscli config backup "/dev/null/blah"
+    run -0 echo "${stderr}"
+    assert_output --partial "Failed to backup configurations: while creating /dev/null/blah: mkdir /dev/null/blah: not a directory"
+
+    # pick a dirpath
     backupdir=$(TMPDIR="${BATS_TEST_TMPDIR}" mktemp -u)
     backupdir=$(TMPDIR="${BATS_TEST_TMPDIR}" mktemp -u)
+
+    # succeed the first time
     run -0 cscli config backup "${backupdir}"
     run -0 cscli config backup "${backupdir}"
     assert_output --partial "Starting configuration backup"
     assert_output --partial "Starting configuration backup"
-    run -1 --separate-stderr cscli config backup "${backupdir}"
 
 
+    # don't overwrite an existing backup
+    run -1 --separate-stderr cscli config backup "${backupdir}"
     run -0 echo "${stderr}"
     run -0 echo "${stderr}"
     assert_output --partial "Failed to backup configurations"
     assert_output --partial "Failed to backup configurations"
     assert_output --partial "file exists"
     assert_output --partial "file exists"
+
+    SIMULATION_YAML="$(config_yq '.config_paths.simulation_path')"
+
+    # restore
+    rm "${SIMULATION_YAML}"
+    run -0 cscli config restore "${backupdir}"
+    assert_file_exist "${SIMULATION_YAML}"
+
+    # cleanup
+    rm -rf -- "${backupdir:?}"
+
+    # backup: detect missing files
+    rm "${SIMULATION_YAML}"
+    run -1 --separate-stderr cscli config backup "${backupdir}"
+    run -0 echo "${stderr}"
+    assert_output --regexp "Failed to backup configurations: failed copy .* to .*: stat .*: no such file or directory"
     rm -rf -- "${backupdir:?}"
     rm -rf -- "${backupdir:?}"
 }
 }
 
 
@@ -244,12 +275,14 @@ declare stderr
     assert_output --partial "# bash completion for cscli"
     assert_output --partial "# bash completion for cscli"
     run -0 cscli completion zsh
     run -0 cscli completion zsh
     assert_output --partial "# zsh completion for cscli"
     assert_output --partial "# zsh completion for cscli"
+    run -0 cscli completion powershell
+    assert_output --partial "# powershell completion for cscli"
+    run -0 cscli completion fish
+    assert_output --partial "# fish completion for cscli"
 
 
     rm "${CONFIG_YAML}"
     rm "${CONFIG_YAML}"
     run -0 cscli completion bash
     run -0 cscli completion bash
     assert_output --partial "# bash completion for cscli"
     assert_output --partial "# bash completion for cscli"
-    run -0 cscli completion zsh
-    assert_output --partial "# zsh completion for cscli"
 }
 }
 
 
 @test "${FILE} cscli hub list" {
 @test "${FILE} cscli hub list" {

+ 57 - 0
tests/bats/01_crowdsec.bats

@@ -0,0 +1,57 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    ./instance-data load
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+# to silence shellcheck
+declare stderr
+
+#----------
+
+@test "${FILE} crowdsec (usage)" {
+    run -0 --separate-stderr timeout 2s "${CROWDSEC}" -h
+    run -0 echo "${stderr}"
+    assert_line --regexp "Usage of .*:"
+
+    run -0 --separate-stderr timeout 2s "${CROWDSEC}" --help
+    run -0 echo "${stderr}"
+    assert_line --regexp "Usage of .*:"
+}
+
+@test "${FILE} crowdsec (unknown flag)" {
+    run -2 --separate-stderr timeout 2s "${CROWDSEC}" --foobar
+    run -0 echo "${stderr}"
+    assert_line "flag provided but not defined: -foobar"
+    assert_line --regexp "Usage of .*"
+}
+
+@test "${FILE} crowdsec (unknown argument)" {
+    run -2 --separate-stderr timeout 2s "${CROWDSEC}" trololo
+    run -0 echo "${stderr}"
+    assert_line "argument provided but not defined: trololo"
+    assert_line --regexp "Usage of .*"
+}
+
+@test "${FILE} crowdsec (no api and no agent)" {
+    run -1 --separate-stderr timeout 2s "${CROWDSEC}" -no-api -no-cs
+    run -0 echo "${stderr}"
+    assert_line --partial "You must run at least the API Server or crowdsec"
+}
+

+ 5 - 0
tests/bats/06_crowdsec.bats

@@ -34,3 +34,8 @@ declare stderr
     assert_output --partial "api server init: unable to run local API: unable to init database client: unknown database type 'meh'"
     assert_output --partial "api server init: unable to run local API: unable to init database client: unknown database type 'meh'"
 }
 }
 
 
+@test "${FILE} CS_LAPI_SECRET not strong enough" {
+    CS_LAPI_SECRET=foo run -1 --separate-stderr timeout 2s "${CROWDSEC}"
+    run -0 echo "${stderr}"
+    assert_output --partial "api server init: unable to run local API: CS_LAPI_SECRET not strong enough"
+}

+ 32 - 0
tests/bats/72_plugin_badconfig.bats

@@ -86,3 +86,35 @@ teardown() {
     assert_output --partial "api server init: unable to run local API: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is world writable, world writable plugins are invalid"
     assert_output --partial "api server init: unable to run local API: while loading plugin: plugin at ${PLUGIN_DIR}/notification-http is world writable, world writable plugins are invalid"
 }
 }
 
 
+@test "${FILE} config.yaml: missing .plugin_config section" {
+    yq e 'del(.plugin_config)' -i "${CONFIG_YAML}"
+    yq e '.notifications=["http_default"]' -i "${PROFILES_PATH}"
+    run -1 --separate-stderr timeout 2s "${CROWDSEC}"
+    run -0 echo "${stderr}"
+    assert_output --partial "api server init: plugins are enabled, but the plugin_config section is missing in the configuration"
+}
+
+@test "${FILE} config.yaml: missing config_paths.notification_dir" {
+    yq e 'del(.config_paths.notification_dir)' -i "${CONFIG_YAML}"
+    yq e '.notifications=["http_default"]' -i "${PROFILES_PATH}"
+    run -1 --separate-stderr timeout 2s "${CROWDSEC}"
+    run -0 echo "${stderr}"
+    assert_output --partial "api server init: plugins are enabled, but config_paths.notification_dir is not defined"
+}
+
+@test "${FILE} config.yaml: missing config_paths.plugin_dir" {
+    yq e 'del(.config_paths.plugin_dir)' -i "${CONFIG_YAML}"
+    yq e '.notifications=["http_default"]' -i "${PROFILES_PATH}"
+    run -1 --separate-stderr timeout 2s "${CROWDSEC}"
+    run -0 echo "${stderr}"
+    assert_output --partial "api server init: plugins are enabled, but config_paths.plugin_dir is not defined"
+}
+
+@test "${FILE} unable to run local API: while reading plugin config" {
+    yq e '.config_paths.notification_dir="/this/path/does/not/exist"' -i "${CONFIG_YAML}"
+    yq e '.notifications=["http_default"]' -i "${PROFILES_PATH}"
+    run -1 --separate-stderr timeout 2s "${CROWDSEC}"
+    run -0 echo "${stderr}"
+    assert_output --partial "api server init: unable to run local API: while loading plugin config: open /this/path/does/not/exist: no such file or directory"
+}
+