瀏覽代碼

Merge branch 'master' into http_plugin_unix_socket

Laurence Jones 1 年之前
父節點
當前提交
0c06438fb6

+ 2 - 2
.github/workflows/bats-hub.yml

@@ -1,4 +1,4 @@
-name: Hub tests
+name: (sub) Bats / Hub
 
 
 on:
 on:
   workflow_call:
   workflow_call:
@@ -17,7 +17,7 @@ jobs:
       matrix:
       matrix:
         test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"]
         test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"]
 
 
-    name: "Build + tests"
+    name: "Functional tests"
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 30
     timeout-minutes: 30
     steps:
     steps:

+ 2 - 2
.github/workflows/bats-mysql.yml

@@ -1,4 +1,4 @@
-name: Functional tests (MySQL)
+name: (sub) Bats / MySQL
 
 
 on:
 on:
   workflow_call:
   workflow_call:
@@ -12,7 +12,7 @@ env:
 
 
 jobs:
 jobs:
   build:
   build:
-    name: "Build + tests"
+    name: "Functional tests"
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 30
     timeout-minutes: 30
     services:
     services:

+ 2 - 2
.github/workflows/bats-postgres.yml

@@ -1,4 +1,4 @@
-name: Functional tests (Postgres)
+name: (sub) Bats / Postgres
 
 
 on:
 on:
   workflow_call:
   workflow_call:
@@ -8,7 +8,7 @@ env:
 
 
 jobs:
 jobs:
   build:
   build:
-    name: "Build + tests"
+    name: "Functional tests"
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 30
     timeout-minutes: 30
     services:
     services:

+ 2 - 2
.github/workflows/bats-sqlite-coverage.yml

@@ -1,4 +1,4 @@
-name: Functional tests (sqlite)
+name: (sub) Bats / sqlite + coverage
 
 
 on:
 on:
   workflow_call:
   workflow_call:
@@ -9,7 +9,7 @@ env:
 
 
 jobs:
 jobs:
   build:
   build:
-    name: "Build + tests"
+    name: "Functional tests"
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 20
     timeout-minutes: 20
 
 

+ 15 - 31
.github/workflows/docker-tests.yml

@@ -15,7 +15,14 @@ on:
       - 'README.md'
       - 'README.md'
 
 
 jobs:
 jobs:
-  test_docker_image:
+  test_flavor:
+    strategy:
+      # we could test all the flavors in a single pytest job,
+      # but let's split them (and the image build) in multiple runners for performance
+      matrix:
+        # can be slim, full or debian (no debian slim).
+        flavor: ["slim", "debian"]
+
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     timeout-minutes: 30
     timeout-minutes: 30
     steps:
     steps:
@@ -30,37 +37,13 @@ jobs:
         with:
         with:
           config: .github/buildkit.toml
           config: .github/buildkit.toml
 
 
-      - name: "Build flavor: slim"
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          file: ./Dockerfile
-          tags: crowdsecurity/crowdsec:test-slim
-          target: slim
-          platforms: linux/amd64
-          load: true
-          cache-from: type=gha
-          cache-to: type=gha,mode=min
-
-      - name: "Build flavor: full"
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          file: ./Dockerfile
-          tags: crowdsecurity/crowdsec:test
-          target: full
-          platforms: linux/amd64
-          load: true
-          cache-from: type=gha
-          cache-to: type=gha,mode=min
-
-      - name: "Build flavor: full (debian)"
+      - name: "Build image"
         uses: docker/build-push-action@v5
         uses: docker/build-push-action@v5
         with:
         with:
           context: .
           context: .
-          file: ./Dockerfile.debian
-          tags: crowdsecurity/crowdsec:test-debian
-          target: full
+          file: ./Dockerfile${{ matrix.flavor == 'debian' && '.debian' || '' }}
+          tags: crowdsecurity/crowdsec:test${{ matrix.flavor == 'full' && '' || '-' }}${{ matrix.flavor == 'full' && '' || matrix.flavor }}
+          target: ${{ matrix.flavor == 'debian' && 'full' || matrix.flavor }}
           platforms: linux/amd64
           platforms: linux/amd64
           load: true
           load: true
           cache-from: type=gha
           cache-from: type=gha
@@ -95,9 +78,10 @@ jobs:
       - name: "Run tests"
       - name: "Run tests"
         env:
         env:
           CROWDSEC_TEST_VERSION: test
           CROWDSEC_TEST_VERSION: test
-          CROWDSEC_TEST_FLAVORS: slim,debian
+          CROWDSEC_TEST_FLAVORS: ${{ matrix.flavor }}
           CROWDSEC_TEST_NETWORK: net-test
           CROWDSEC_TEST_NETWORK: net-test
           CROWDSEC_TEST_TIMEOUT: 90
           CROWDSEC_TEST_TIMEOUT: 90
+        # running serially to reduce test flakiness
         run: |
         run: |
           cd docker/test
           cd docker/test
-          pipenv run pytest -n 2 --durations=0 --color=yes
+          pipenv run pytest -n 1 --durations=0 --color=yes

+ 3 - 3
.github/workflows/publish-docker-master.yml

@@ -1,4 +1,4 @@
-name: Publish Docker image on Push to Master
+name: (push-master) Publish latest Docker images
 
 
 on:
 on:
   push:
   push:
@@ -6,10 +6,10 @@ on:
     paths:
     paths:
       - 'pkg/**'
       - 'pkg/**'
       - 'cmd/**'
       - 'cmd/**'
-      - 'plugins/**'
+      - 'mk/**'
       - 'docker/docker_start.sh'
       - 'docker/docker_start.sh'
       - 'docker/config.yaml'
       - 'docker/config.yaml'
-      - '.github/workflows/publish_docker-master.yml'
+      - '.github/workflows/publish-docker-master.yml'
       - '.github/workflows/publish-docker.yml'
       - '.github/workflows/publish-docker.yml'
       - 'Dockerfile'
       - 'Dockerfile'
       - 'Dockerfile.debian'
       - 'Dockerfile.debian'

+ 3 - 11
.github/workflows/publish-docker-release.yml

@@ -1,4 +1,4 @@
-name: Publish Docker images
+name: (manual) Publish Docker images
 
 
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
@@ -20,37 +20,29 @@ on:
 
 
 jobs:
 jobs:
   alpine:
   alpine:
-    strategy:
-      matrix:
-        platform: ["linux/amd64", "linux/386", "linux/arm64", "linux/arm/v7", "linux/arm/v6"]
-
     uses: ./.github/workflows/publish-docker.yml
     uses: ./.github/workflows/publish-docker.yml
     secrets:
     secrets:
       DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
       DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
       DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
       DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
     with:
     with:
-      platform: ${{ matrix.platform }}
       image_version: ${{ github.event.inputs.image_version }}
       image_version: ${{ github.event.inputs.image_version }}
       crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
       crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
       latest: ${{ github.event.inputs.latest == 'true' }}
       latest: ${{ github.event.inputs.latest == 'true' }}
       push: ${{ github.event.inputs.push == 'true' }}
       push: ${{ github.event.inputs.push == 'true' }}
       slim: true
       slim: true
       debian: false
       debian: false
+      platform: "linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6"
 
 
   debian:
   debian:
-    strategy:
-      matrix:
-        platform: ["linux/amd64", "linux/386", "linux/arm64"]
-
     uses: ./.github/workflows/publish-docker.yml
     uses: ./.github/workflows/publish-docker.yml
     secrets:
     secrets:
       DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
       DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
       DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
       DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
     with:
     with:
-      platform: ${{ matrix.platform }}
       image_version: ${{ github.event.inputs.image_version }}
       image_version: ${{ github.event.inputs.image_version }}
       crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
       crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
       latest: ${{ github.event.inputs.latest == 'true' }}
       latest: ${{ github.event.inputs.latest == 'true' }}
       push: ${{ github.event.inputs.push == 'true' }}
       push: ${{ github.event.inputs.push == 'true' }}
       slim: false
       slim: false
       debian: true
       debian: true
+      platform: "linux/amd64,linux/386,linux/arm64"

+ 1 - 1
.github/workflows/publish-docker.yml

@@ -1,4 +1,4 @@
-name: Publish Docker image / platform
+name: (sub) Publish Docker images
 
 
 on:
 on:
   workflow_call:
   workflow_call:

+ 1 - 1
.github/workflows/update_docker_hub_doc.yml

@@ -1,4 +1,4 @@
-name: Update Docker Hub README
+name: (push-master) Update Docker Hub README
 
 
 on:
 on:
   push:
   push:

+ 9 - 8
Dockerfile

@@ -39,10 +39,8 @@ RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/co
     mkdir -p /staging/var/lib/crowdsec && \
     mkdir -p /staging/var/lib/crowdsec && \
     mkdir -p /var/lib/crowdsec/data
     mkdir -p /var/lib/crowdsec/data
 
 
-COPY --from=build /go/bin/yq /usr/local/bin/yq
+COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
 COPY --from=build /etc/crowdsec /staging/etc/crowdsec
 COPY --from=build /etc/crowdsec /staging/etc/crowdsec
-COPY --from=build /usr/local/bin/crowdsec /usr/local/bin/crowdsec
-COPY --from=build /usr/local/bin/cscli /usr/local/bin/cscli
 COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
 COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
 COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
 COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
 RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml
 RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml
@@ -53,11 +51,14 @@ FROM slim as plugins
 
 
 # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
 # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
 # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
 # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
-COPY --from=build /go/src/crowdsec/cmd/notification-email/email.yaml /staging/etc/crowdsec/notifications/email.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-http/http.yaml /staging/etc/crowdsec/notifications/http.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-slack/slack.yaml /staging/etc/crowdsec/notifications/slack.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-splunk/splunk.yaml /staging/etc/crowdsec/notifications/splunk.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml /staging/etc/crowdsec/notifications/sentinel.yaml
+COPY --from=build \
+    /go/src/crowdsec/cmd/notification-email/email.yaml \
+    /go/src/crowdsec/cmd/notification-http/http.yaml \
+    /go/src/crowdsec/cmd/notification-slack/slack.yaml \
+    /go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
+    /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
+    /staging/etc/crowdsec/notifications/
+
 COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
 COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
 
 
 FROM slim as geoip
 FROM slim as geoip

+ 9 - 8
Dockerfile.debian

@@ -55,10 +55,8 @@ RUN apt-get update && \
     mkdir -p /staging/var/lib/crowdsec && \
     mkdir -p /staging/var/lib/crowdsec && \
     mkdir -p /var/lib/crowdsec/data
     mkdir -p /var/lib/crowdsec/data
 
 
-COPY --from=build /go/bin/yq /usr/local/bin/yq
+COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
 COPY --from=build /etc/crowdsec /staging/etc/crowdsec
 COPY --from=build /etc/crowdsec /staging/etc/crowdsec
-COPY --from=build /usr/local/bin/crowdsec /usr/local/bin/crowdsec
-COPY --from=build /usr/local/bin/cscli /usr/local/bin/cscli
 COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
 COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
 COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
 COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
 RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml && \
 RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml && \
@@ -70,11 +68,14 @@ FROM slim as plugins
 
 
 # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
 # Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
 # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
 # The files are here for reference, as users will need to mount a new version to be actually able to use notifications
-COPY --from=build /go/src/crowdsec/cmd/notification-email/email.yaml /staging/etc/crowdsec/notifications/email.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-http/http.yaml /staging/etc/crowdsec/notifications/http.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-slack/slack.yaml /staging/etc/crowdsec/notifications/slack.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-splunk/splunk.yaml /staging/etc/crowdsec/notifications/splunk.yaml
-COPY --from=build /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml /staging/etc/crowdsec/notifications/sentinel.yaml
+COPY --from=build \
+    /go/src/crowdsec/cmd/notification-email/email.yaml \
+    /go/src/crowdsec/cmd/notification-http/http.yaml \
+    /go/src/crowdsec/cmd/notification-slack/slack.yaml \
+    /go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
+    /go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
+    /staging/etc/crowdsec/notifications/
+
 COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
 COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
 
 
 FROM slim as geoip
 FROM slim as geoip

+ 59 - 102
cmd/crowdsec-cli/alerts.go

@@ -11,7 +11,6 @@ import (
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"text/template"
 	"text/template"
-	"time"
 
 
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	"github.com/go-openapi/strfmt"
 	"github.com/go-openapi/strfmt"
@@ -48,52 +47,9 @@ func DecisionsFromAlert(alert *models.Alert) string {
 	return ret
 	return ret
 }
 }
 
 
-func DateFromAlert(alert *models.Alert) string {
-	ts, err := time.Parse(time.RFC3339, alert.CreatedAt)
-	if err != nil {
-		log.Infof("while parsing %s with %s : %s", alert.CreatedAt, time.RFC3339, err)
-		return alert.CreatedAt
-	}
-	return ts.Format(time.RFC822)
-}
-
-func SourceFromAlert(alert *models.Alert) string {
-
-	//more than one item, just number and scope
-	if len(alert.Decisions) > 1 {
-		return fmt.Sprintf("%d %ss (%s)", len(alert.Decisions), *alert.Decisions[0].Scope, *alert.Decisions[0].Origin)
-	}
-
-	//fallback on single decision information
-	if len(alert.Decisions) == 1 {
-		return fmt.Sprintf("%s:%s", *alert.Decisions[0].Scope, *alert.Decisions[0].Value)
-	}
-
-	//try to compose a human friendly version
-	if *alert.Source.Value != "" && *alert.Source.Scope != "" {
-		scope := fmt.Sprintf("%s:%s", *alert.Source.Scope, *alert.Source.Value)
-		extra := ""
-		if alert.Source.Cn != "" {
-			extra = alert.Source.Cn
-		}
-		if alert.Source.AsNumber != "" {
-			extra += fmt.Sprintf("/%s", alert.Source.AsNumber)
-		}
-		if alert.Source.AsName != "" {
-			extra += fmt.Sprintf("/%s", alert.Source.AsName)
-		}
-
-		if extra != "" {
-			scope += " (" + extra + ")"
-		}
-		return scope
-	}
-	return ""
-}
-
-func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
-
-	if csConfig.Cscli.Output == "raw" {
+func alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
+	switch csConfig.Cscli.Output {
+	case "raw":
 		csvwriter := csv.NewWriter(os.Stdout)
 		csvwriter := csv.NewWriter(os.Stdout)
 		header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
 		header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
 		if printMachine {
 		if printMachine {
@@ -123,7 +79,7 @@ func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
 			}
 			}
 		}
 		}
 		csvwriter.Flush()
 		csvwriter.Flush()
-	} else if csConfig.Cscli.Output == "json" {
+	case "json":
 		if *alerts == nil {
 		if *alerts == nil {
 			// avoid returning "null" in json
 			// avoid returning "null" in json
 			// could be cleaner if we used slice of alerts directly
 			// could be cleaner if we used slice of alerts directly
@@ -131,8 +87,8 @@ func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
 			return nil
 			return nil
 		}
 		}
 		x, _ := json.MarshalIndent(alerts, "", " ")
 		x, _ := json.MarshalIndent(alerts, "", " ")
-		fmt.Printf("%s", string(x))
-	} else if csConfig.Cscli.Output == "human" {
+		fmt.Print(string(x))
+	case "human":
 		if len(*alerts) == 0 {
 		if len(*alerts) == 0 {
 			fmt.Println("No active alerts")
 			fmt.Println("No active alerts")
 			return nil
 			return nil
@@ -160,59 +116,60 @@ var alertTemplate = `
 
 
 `
 `
 
 
-func DisplayOneAlert(alert *models.Alert, withDetail bool) error {
-	if csConfig.Cscli.Output == "human" {
-		tmpl, err := template.New("alert").Parse(alertTemplate)
-		if err != nil {
-			return err
-		}
-		err = tmpl.Execute(os.Stdout, alert)
-		if err != nil {
-			return err
-		}
-
-		alertDecisionsTable(color.Output, alert)
+func displayOneAlert(alert *models.Alert, withDetail bool) error {
+	tmpl, err := template.New("alert").Parse(alertTemplate)
+	if err != nil {
+		return err
+	}
+	err = tmpl.Execute(os.Stdout, alert)
+	if err != nil {
+		return err
+	}
 
 
-		if len(alert.Meta) > 0 {
-			fmt.Printf("\n - Context  :\n")
-			sort.Slice(alert.Meta, func(i, j int) bool {
-				return alert.Meta[i].Key < alert.Meta[j].Key
-			})
-			table := newTable(color.Output)
-			table.SetRowLines(false)
-			table.SetHeaders("Key", "Value")
-			for _, meta := range alert.Meta {
-				var valSlice []string
-				if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
-					return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
-				}
-				for _, value := range valSlice {
-					table.AddRow(
-						meta.Key,
-						value,
-					)
-				}
+	alertDecisionsTable(color.Output, alert)
+
+	if len(alert.Meta) > 0 {
+		fmt.Printf("\n - Context  :\n")
+		sort.Slice(alert.Meta, func(i, j int) bool {
+			return alert.Meta[i].Key < alert.Meta[j].Key
+		})
+		table := newTable(color.Output)
+		table.SetRowLines(false)
+		table.SetHeaders("Key", "Value")
+		for _, meta := range alert.Meta {
+			var valSlice []string
+			if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
+				return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
+			}
+			for _, value := range valSlice {
+				table.AddRow(
+					meta.Key,
+					value,
+				)
 			}
 			}
-			table.Render()
 		}
 		}
+		table.Render()
+	}
 
 
-		if withDetail {
-			fmt.Printf("\n - Events  :\n")
-			for _, event := range alert.Events {
-				alertEventTable(color.Output, event)
-			}
+	if withDetail {
+		fmt.Printf("\n - Events  :\n")
+		for _, event := range alert.Events {
+			alertEventTable(color.Output, event)
 		}
 		}
 	}
 	}
+
 	return nil
 	return nil
 }
 }
 
 
-type cliAlerts struct{}
+type cliAlerts struct{
+	client *apiclient.ApiClient
+}
 
 
 func NewCLIAlerts() *cliAlerts {
 func NewCLIAlerts() *cliAlerts {
 	return &cliAlerts{}
 	return &cliAlerts{}
 }
 }
 
 
-func (cli cliAlerts) NewCommand() *cobra.Command {
+func (cli *cliAlerts) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "alerts [action]",
 		Use:               "alerts [action]",
 		Short:             "Manage alerts",
 		Short:             "Manage alerts",
@@ -228,7 +185,7 @@ func (cli cliAlerts) NewCommand() *cobra.Command {
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("parsing api url %s: %w", apiURL, err)
 				return fmt.Errorf("parsing api url %s: %w", apiURL, err)
 			}
 			}
-			Client, err = apiclient.NewClient(&apiclient.Config{
+			cli.client, err = apiclient.NewClient(&apiclient.Config{
 				MachineID:     csConfig.API.Client.Credentials.Login,
 				MachineID:     csConfig.API.Client.Credentials.Login,
 				Password:      strfmt.Password(csConfig.API.Client.Credentials.Password),
 				Password:      strfmt.Password(csConfig.API.Client.Credentials.Password),
 				UserAgent:     fmt.Sprintf("crowdsec/%s", version.String()),
 				UserAgent:     fmt.Sprintf("crowdsec/%s", version.String()),
@@ -251,7 +208,7 @@ func (cli cliAlerts) NewCommand() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliAlerts) NewListCmd() *cobra.Command {
+func (cli *cliAlerts) NewListCmd() *cobra.Command {
 	var alertListFilter = apiclient.AlertsListOpts{
 	var alertListFilter = apiclient.AlertsListOpts{
 		ScopeEquals:    new(string),
 		ScopeEquals:    new(string),
 		ValueEquals:    new(string),
 		ValueEquals:    new(string),
@@ -345,12 +302,12 @@ cscli alerts list --type ban`,
 				alertListFilter.Contains = new(bool)
 				alertListFilter.Contains = new(bool)
 			}
 			}
 
 
-			alerts, _, err := Client.Alerts.List(context.Background(), alertListFilter)
+			alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("unable to list alerts: %v", err)
 				return fmt.Errorf("unable to list alerts: %v", err)
 			}
 			}
 
 
-			err = AlertsToTable(alerts, printMachine)
+			err = alertsToTable(alerts, printMachine)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("unable to list alerts: %v", err)
 				return fmt.Errorf("unable to list alerts: %v", err)
 			}
 			}
@@ -376,7 +333,7 @@ cscli alerts list --type ban`,
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliAlerts) NewDeleteCmd() *cobra.Command {
+func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
 	var ActiveDecision *bool
 	var ActiveDecision *bool
 	var AlertDeleteAll bool
 	var AlertDeleteAll bool
 	var delAlertByID string
 	var delAlertByID string
@@ -451,12 +408,12 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
 
 
 			var alerts *models.DeleteAlertsResponse
 			var alerts *models.DeleteAlertsResponse
 			if delAlertByID == "" {
 			if delAlertByID == "" {
-				alerts, _, err = Client.Alerts.Delete(context.Background(), alertDeleteFilter)
+				alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
 				if err != nil {
 				if err != nil {
 					return fmt.Errorf("unable to delete alerts : %v", err)
 					return fmt.Errorf("unable to delete alerts : %v", err)
 				}
 				}
 			} else {
 			} else {
-				alerts, _, err = Client.Alerts.DeleteOne(context.Background(), delAlertByID)
+				alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
 				if err != nil {
 				if err != nil {
 					return fmt.Errorf("unable to delete alert: %v", err)
 					return fmt.Errorf("unable to delete alert: %v", err)
 				}
 				}
@@ -478,7 +435,7 @@ cscli alerts delete -s crowdsecurity/ssh-bf"`,
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliAlerts) NewInspectCmd() *cobra.Command {
+func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
 	var details bool
 	var details bool
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               `inspect "alert_id"`,
 		Use:               `inspect "alert_id"`,
@@ -495,13 +452,13 @@ func (cli cliAlerts) NewInspectCmd() *cobra.Command {
 				if err != nil {
 				if err != nil {
 					return fmt.Errorf("bad alert id %s", alertID)
 					return fmt.Errorf("bad alert id %s", alertID)
 				}
 				}
-				alert, _, err := Client.Alerts.GetByID(context.Background(), id)
+				alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
 				if err != nil {
 				if err != nil {
 					return fmt.Errorf("can't find alert with id %s: %s", alertID, err)
 					return fmt.Errorf("can't find alert with id %s: %s", alertID, err)
 				}
 				}
 				switch csConfig.Cscli.Output {
 				switch csConfig.Cscli.Output {
 				case "human":
 				case "human":
-					if err := DisplayOneAlert(alert, details); err != nil {
+					if err := displayOneAlert(alert, details); err != nil {
 						continue
 						continue
 					}
 					}
 				case "json":
 				case "json":
@@ -528,7 +485,7 @@ func (cli cliAlerts) NewInspectCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliAlerts) NewFlushCmd() *cobra.Command {
+func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
 	var maxItems int
 	var maxItems int
 	var maxAge string
 	var maxAge string
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
@@ -542,12 +499,12 @@ func (cli cliAlerts) NewFlushCmd() *cobra.Command {
 			if err := require.LAPI(csConfig); err != nil {
 			if err := require.LAPI(csConfig); err != nil {
 				return err
 				return err
 			}
 			}
-			dbClient, err = database.NewClient(csConfig.DbConfig)
+			db, err := database.NewClient(csConfig.DbConfig)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("unable to create new database client: %s", err)
 				return fmt.Errorf("unable to create new database client: %s", err)
 			}
 			}
 			log.Info("Flushing alerts. !! This may take a long time !!")
 			log.Info("Flushing alerts. !! This may take a long time !!")
-			err = dbClient.FlushAlerts(maxAge, maxItems)
+			err = db.FlushAlerts(maxAge, maxItems)
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("unable to flush alerts: %s", err)
 				return fmt.Errorf("unable to flush alerts: %s", err)
 			}
 			}

+ 186 - 167
cmd/crowdsec-cli/bouncers.go

@@ -4,7 +4,8 @@ import (
 	"encoding/csv"
 	"encoding/csv"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"io"
+	"os"
+	"slices"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -12,7 +13,6 @@ import (
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
-	"slices"
 
 
 	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
 	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
 	middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
 	middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
@@ -20,53 +20,33 @@ import (
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 )
 )
 
 
-func getBouncers(out io.Writer, dbClient *database.Client) error {
-	bouncers, err := dbClient.ListBouncers()
-	if err != nil {
-		return fmt.Errorf("unable to list bouncers: %s", err)
+func askYesNo(message string, defaultAnswer bool) (bool, error) {
+	var answer bool
+
+	prompt := &survey.Confirm{
+		Message: message,
+		Default: defaultAnswer,
 	}
 	}
 
 
-	switch csConfig.Cscli.Output {
-	case "human":
-		getBouncersTable(out, bouncers)
-	case "json":
-		enc := json.NewEncoder(out)
-		enc.SetIndent("", "  ")
-		if err := enc.Encode(bouncers); err != nil {
-			return fmt.Errorf("failed to unmarshal: %w", err)
-		}
-		return nil
-	case "raw":
-		csvwriter := csv.NewWriter(out)
-		err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"})
-		if err != nil {
-			return fmt.Errorf("failed to write raw header: %w", err)
-		}
-		for _, b := range bouncers {
-			var revoked string
-			if !b.Revoked {
-				revoked = "validated"
-			} else {
-				revoked = "pending"
-			}
-			err := csvwriter.Write([]string{b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType})
-			if err != nil {
-				return fmt.Errorf("failed to write raw: %w", err)
-			}
-		}
-		csvwriter.Flush()
+	if err := survey.AskOne(prompt, &answer); err != nil {
+		return defaultAnswer, err
 	}
 	}
 
 
-	return nil
+	return answer, nil
 }
 }
 
 
-type cliBouncers struct {}
+type cliBouncers struct {
+	db *database.Client
+	cfg configGetter
+}
 
 
-func NewCLIBouncers() *cliBouncers {
-	return &cliBouncers{}
+func NewCLIBouncers(getconfig configGetter) *cliBouncers {
+	return &cliBouncers{
+		cfg: getconfig,
+	}
 }
 }
 
 
-func (cli cliBouncers) NewCommand() *cobra.Command {
+func (cli *cliBouncers) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "bouncers [action]",
 		Use:   "bouncers [action]",
 		Short: "Manage bouncers [requires local API]",
 		Short: "Manage bouncers [requires local API]",
@@ -76,94 +56,127 @@ Note: This command requires database direct access, so is intended to be run on
 		Args:              cobra.MinimumNArgs(1),
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"bouncer"},
 		Aliases:           []string{"bouncer"},
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
 			var err error
 			var err error
-			if err = require.LAPI(csConfig); err != nil {
+			if err = require.LAPI(cli.cfg()); err != nil {
 				return err
 				return err
 			}
 			}
 
 
-			dbClient, err = database.NewClient(csConfig.DbConfig)
+			cli.db, err = database.NewClient(cli.cfg().DbConfig)
 			if err != nil {
 			if err != nil {
-				return fmt.Errorf("unable to create new database client: %s", err)
+				return fmt.Errorf("can't connect to the database: %s", err)
 			}
 			}
+
 			return nil
 			return nil
 		},
 		},
 	}
 	}
 
 
-	cmd.AddCommand(cli.NewListCmd())
-	cmd.AddCommand(cli.NewAddCmd())
-	cmd.AddCommand(cli.NewDeleteCmd())
-	cmd.AddCommand(cli.NewPruneCmd())
+	cmd.AddCommand(cli.newListCmd())
+	cmd.AddCommand(cli.newAddCmd())
+	cmd.AddCommand(cli.newDeleteCmd())
+	cmd.AddCommand(cli.newPruneCmd())
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliBouncers) NewListCmd() *cobra.Command {
+func (cli *cliBouncers) list() error {
+	out := color.Output
+
+	bouncers, err := cli.db.ListBouncers()
+	if err != nil {
+		return fmt.Errorf("unable to list bouncers: %s", err)
+	}
+
+	switch cli.cfg().Cscli.Output {
+	case "human":
+		getBouncersTable(out, bouncers)
+	case "json":
+		enc := json.NewEncoder(out)
+		enc.SetIndent("", "  ")
+
+		if err := enc.Encode(bouncers); err != nil {
+			return fmt.Errorf("failed to marshal: %w", err)
+		}
+
+		return nil
+	case "raw":
+		csvwriter := csv.NewWriter(out)
+
+		if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
+			return fmt.Errorf("failed to write raw header: %w", err)
+		}
+
+		for _, b := range bouncers {
+			valid := "validated"
+			if b.Revoked {
+				valid = "pending"
+			}
+
+			if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}); err != nil {
+				return fmt.Errorf("failed to write raw: %w", err)
+			}
+		}
+
+		csvwriter.Flush()
+	}
+
+	return nil
+}
+
+func (cli *cliBouncers) newListCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "list",
 		Use:               "list",
 		Short:             "list all bouncers within the database",
 		Short:             "list all bouncers within the database",
 		Example:           `cscli bouncers list`,
 		Example:           `cscli bouncers list`,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE: func(cmd *cobra.Command, arg []string) error {
-			err := getBouncers(color.Output, dbClient)
-			if err != nil {
-				return fmt.Errorf("unable to list bouncers: %s", err)
-			}
-			return nil
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.list()
 		},
 		},
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliBouncers) add(cmd *cobra.Command, args []string) error {
-	keyLength := 32
-
-	flags := cmd.Flags()
-
-	key, err := flags.GetString("key")
-	if err != nil {
-		return err
-	}
+func (cli *cliBouncers) add(bouncerName string, key string) error {
+	var err error
 
 
-	keyName := args[0]
-	var apiKey string
+	keyLength := 32
 
 
-	if keyName == "" {
-		return fmt.Errorf("please provide a name for the api key")
-	}
-	apiKey = key
 	if key == "" {
 	if key == "" {
-		apiKey, err = middlewares.GenerateAPIKey(keyLength)
-	}
-	if err != nil {
-		return fmt.Errorf("unable to generate api key: %s", err)
+		key, err = middlewares.GenerateAPIKey(keyLength)
+		if err != nil {
+			return fmt.Errorf("unable to generate api key: %s", err)
+		}
 	}
 	}
-	_, err = dbClient.CreateBouncer(keyName, "", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType)
+
+	_, err = cli.db.CreateBouncer(bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to create bouncer: %s", err)
 		return fmt.Errorf("unable to create bouncer: %s", err)
 	}
 	}
 
 
-	switch csConfig.Cscli.Output {
+	switch cli.cfg().Cscli.Output {
 	case "human":
 	case "human":
-		fmt.Printf("API key for '%s':\n\n", keyName)
-		fmt.Printf("   %s\n\n", apiKey)
+		fmt.Printf("API key for '%s':\n\n", bouncerName)
+		fmt.Printf("   %s\n\n", key)
 		fmt.Print("Please keep this key since you will not be able to retrieve it!\n")
 		fmt.Print("Please keep this key since you will not be able to retrieve it!\n")
 	case "raw":
 	case "raw":
-		fmt.Printf("%s", apiKey)
+		fmt.Print(key)
 	case "json":
 	case "json":
-		j, err := json.Marshal(apiKey)
+		j, err := json.Marshal(key)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("unable to marshal api key")
 			return fmt.Errorf("unable to marshal api key")
 		}
 		}
-		fmt.Printf("%s", string(j))
+
+		fmt.Print(string(j))
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliBouncers) NewAddCmd() *cobra.Command {
+func (cli *cliBouncers) newAddCmd() *cobra.Command {
+	var key string
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "add MyBouncerName",
 		Use:   "add MyBouncerName",
 		Short: "add a single bouncer to the database",
 		Short: "add a single bouncer to the database",
@@ -171,127 +184,133 @@ func (cli cliBouncers) NewAddCmd() *cobra.Command {
 cscli bouncers add MyBouncerName --key <random-key>`,
 cscli bouncers add MyBouncerName --key <random-key>`,
 		Args:              cobra.ExactArgs(1),
 		Args:              cobra.ExactArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.add,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cli.add(args[0], key)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
 	flags.StringP("length", "l", "", "length of the api key")
 	flags.StringP("length", "l", "", "length of the api key")
 	flags.MarkDeprecated("length", "use --key instead")
 	flags.MarkDeprecated("length", "use --key instead")
-	flags.StringP("key", "k", "", "api key for the bouncer")
+	flags.StringVarP(&key, "key", "k", "", "api key for the bouncer")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliBouncers) delete(cmd *cobra.Command, args []string) error {
-	for _, bouncerID := range args {
-		err := dbClient.DeleteBouncer(bouncerID)
+func (cli *cliBouncers) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	bouncers, err := cli.db.ListBouncers()
+	if err != nil {
+		cobra.CompError("unable to list bouncers " + err.Error())
+	}
+
+	ret :=[]string{}
+
+	for _, bouncer := range bouncers {
+		if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
+			ret = append(ret, bouncer.Name)
+		}
+	}
+
+	return ret, cobra.ShellCompDirectiveNoFileComp
+}
+
+func (cli *cliBouncers) delete(bouncers []string) error {
+	for _, bouncerID := range bouncers {
+		err := cli.db.DeleteBouncer(bouncerID)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("unable to delete bouncer '%s': %s", bouncerID, err)
 			return fmt.Errorf("unable to delete bouncer '%s': %s", bouncerID, err)
 		}
 		}
+
 		log.Infof("bouncer '%s' deleted successfully", bouncerID)
 		log.Infof("bouncer '%s' deleted successfully", bouncerID)
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliBouncers) NewDeleteCmd() *cobra.Command {
+func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "delete MyBouncerName",
 		Use:               "delete MyBouncerName",
 		Short:             "delete bouncer(s) from the database",
 		Short:             "delete bouncer(s) from the database",
 		Args:              cobra.MinimumNArgs(1),
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"remove"},
 		Aliases:           []string{"remove"},
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-			var err error
-			dbClient, err = getDBClient()
-			if err != nil {
-				cobra.CompError("unable to create new database client: " + err.Error())
-				return nil, cobra.ShellCompDirectiveNoFileComp
-			}
-			bouncers, err := dbClient.ListBouncers()
-			if err != nil {
-				cobra.CompError("unable to list bouncers " + err.Error())
-			}
-			ret := make([]string, 0)
-			for _, bouncer := range bouncers {
-				if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
-					ret = append(ret, bouncer.Name)
-				}
-			}
-			return ret, cobra.ShellCompDirectiveNoFileComp
+		ValidArgsFunction: cli.deleteValid,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cli.delete(args)
 		},
 		},
-		RunE: cli.delete,
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliBouncers) NewPruneCmd() *cobra.Command {
-	var parsedDuration time.Duration
+func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
+	if duration < 2*time.Minute {
+		if yes, err := askYesNo(
+				"The duration you provided is less than 2 minutes. " +
+				"This may remove active bouncers. Continue?", false); err != nil {
+			return err
+		} else if !yes {
+			fmt.Println("User aborted prune. No changes were made.")
+			return nil
+		}
+	}
+
+	bouncers, err := cli.db.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(duration))
+	if err != nil {
+		return fmt.Errorf("unable to query bouncers: %w", err)
+	}
+
+	if len(bouncers) == 0 {
+		fmt.Println("No bouncers to prune.")
+		return nil
+	}
+
+	getBouncersTable(color.Output, bouncers)
+
+	if !force {
+		if yes, err := askYesNo(
+				"You are about to PERMANENTLY remove the above bouncers from the database. " +
+				"These will NOT be recoverable. Continue?", false); err != nil {
+			return err
+		} else if !yes {
+			fmt.Println("User aborted prune. No changes were made.")
+			return nil
+		}
+	}
+
+	deleted, err := cli.db.BulkDeleteBouncers(bouncers)
+	if err != nil {
+		return fmt.Errorf("unable to prune bouncers: %s", err)
+	}
+
+	fmt.Fprintf(os.Stderr, "Successfully deleted %d bouncers\n", deleted)
+
+	return nil
+}
+
+func (cli *cliBouncers) newPruneCmd() *cobra.Command {
+	var (
+		duration time.Duration
+		force    bool
+	)
+
+	const defaultDuration = 60 * time.Minute
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "prune",
 		Use:               "prune",
 		Short:             "prune multiple bouncers from the database",
 		Short:             "prune multiple bouncers from the database",
 		Args:              cobra.NoArgs,
 		Args:              cobra.NoArgs,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Example: `cscli bouncers prune -d 60m
-cscli bouncers prune -d 60m --force`,
-		PreRunE: func(cmd *cobra.Command, args []string) error {
-			dur, _ := cmd.Flags().GetString("duration")
-			var err error
-			parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur))
-			if err != nil {
-				return fmt.Errorf("unable to parse duration '%s': %s", dur, err)
-			}
-			return nil
-		},
-		RunE: func(cmd *cobra.Command, args []string) error {
-			force, _ := cmd.Flags().GetBool("force")
-			if parsedDuration >= 0-2*time.Minute {
-				var answer bool
-				prompt := &survey.Confirm{
-					Message: "The duration you provided is less than or equal 2 minutes this may remove active bouncers continue ?",
-					Default: false,
-				}
-				if err := survey.AskOne(prompt, &answer); err != nil {
-					return fmt.Errorf("unable to ask about prune check: %s", err)
-				}
-				if !answer {
-					fmt.Println("user aborted prune no changes were made")
-					return nil
-				}
-			}
-			bouncers, err := dbClient.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(parsedDuration))
-			if err != nil {
-				return fmt.Errorf("unable to query bouncers: %s", err)
-			}
-			if len(bouncers) == 0 {
-				fmt.Println("no bouncers to prune")
-				return nil
-			}
-			getBouncersTable(color.Output, bouncers)
-			if !force {
-				var answer bool
-				prompt := &survey.Confirm{
-					Message: "You are about to PERMANENTLY remove the above bouncers from the database these will NOT be recoverable, continue ?",
-					Default: false,
-				}
-				if err := survey.AskOne(prompt, &answer); err != nil {
-					return fmt.Errorf("unable to ask about prune check: %s", err)
-				}
-				if !answer {
-					fmt.Println("user aborted prune no changes were made")
-					return nil
-				}
-			}
-			nbDeleted, err := dbClient.BulkDeleteBouncers(bouncers)
-			if err != nil {
-				return fmt.Errorf("unable to prune bouncers: %s", err)
-			}
-			fmt.Printf("successfully delete %d bouncers\n", nbDeleted)
-			return nil
+		Example: `cscli bouncers prune -d 45m
+cscli bouncers prune -d 45m --force`,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.prune(duration, force)
 		},
 		},
 	}
 	}
-	cmd.Flags().StringP("duration", "d", "60m", "duration of time since last pull")
-	cmd.Flags().Bool("force", false, "force prune without asking for confirmation")
+
+	flags := cmd.Flags()
+	flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since last pull")
+	flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
+
 	return cmd
 	return cmd
 }
 }

+ 55 - 45
cmd/crowdsec-cli/dashboard.go

@@ -43,14 +43,17 @@ var (
 	// information needed to set up a random password on user's behalf
 	// information needed to set up a random password on user's behalf
 )
 )
 
 
-type cliDashboard struct{}
+type cliDashboard struct{
+	cfg configGetter
+}
 
 
-func NewCLIDashboard() *cliDashboard {
-	return &cliDashboard{}
+func NewCLIDashboard(getconfig configGetter) *cliDashboard {
+	return &cliDashboard{
+		cfg: getconfig,
+	}
 }
 }
 
 
-func (cli cliDashboard) NewCommand() *cobra.Command {
-	/* ---- UPDATE COMMAND */
+func (cli *cliDashboard) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "dashboard [command]",
 		Use:   "dashboard [command]",
 		Short: "Manage your metabase dashboard container [requires local API]",
 		Short: "Manage your metabase dashboard container [requires local API]",
@@ -65,8 +68,9 @@ cscli dashboard start
 cscli dashboard stop
 cscli dashboard stop
 cscli dashboard remove
 cscli dashboard remove
 `,
 `,
-		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			if err := require.LAPI(csConfig); err != nil {
+		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
+			cfg := cli.cfg()
+			if err := require.LAPI(cfg); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -74,13 +78,13 @@ cscli dashboard remove
 				return err
 				return err
 			}
 			}
 
 
-			metabaseConfigFolderPath := filepath.Join(csConfig.ConfigPaths.ConfigDir, metabaseConfigFolder)
+			metabaseConfigFolderPath := filepath.Join(cfg.ConfigPaths.ConfigDir, metabaseConfigFolder)
 			metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
 			metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
 			if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
 			if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
 				return err
 				return err
 			}
 			}
 
 
-			if err := require.DB(csConfig); err != nil {
+			if err := require.DB(cfg); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -99,16 +103,16 @@ cscli dashboard remove
 		},
 		},
 	}
 	}
 
 
-	cmd.AddCommand(cli.NewSetupCmd())
-	cmd.AddCommand(cli.NewStartCmd())
-	cmd.AddCommand(cli.NewStopCmd())
-	cmd.AddCommand(cli.NewShowPasswordCmd())
-	cmd.AddCommand(cli.NewRemoveCmd())
+	cmd.AddCommand(cli.newSetupCmd())
+	cmd.AddCommand(cli.newStartCmd())
+	cmd.AddCommand(cli.newStopCmd())
+	cmd.AddCommand(cli.newShowPasswordCmd())
+	cmd.AddCommand(cli.newRemoveCmd())
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliDashboard) NewSetupCmd() *cobra.Command {
+func (cli *cliDashboard) newSetupCmd() *cobra.Command {
 	var force bool
 	var force bool
 
 
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
@@ -122,9 +126,9 @@ cscli dashboard setup
 cscli dashboard setup --listen 0.0.0.0
 cscli dashboard setup --listen 0.0.0.0
 cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
 cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
  `,
  `,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(_ *cobra.Command, _ []string) error {
 			if metabaseDbPath == "" {
 			if metabaseDbPath == "" {
-				metabaseDbPath = csConfig.ConfigPaths.DataDir
+				metabaseDbPath = cli.cfg().ConfigPaths.DataDir
 			}
 			}
 
 
 			if metabasePassword == "" {
 			if metabasePassword == "" {
@@ -145,10 +149,10 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
-			if err = chownDatabase(dockerGroup.Gid); err != nil {
+			if err = cli.chownDatabase(dockerGroup.Gid); err != nil {
 				return err
 				return err
 			}
 			}
-			mb, err := metabase.SetupMetabase(csConfig.API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
+			mb, err := metabase.SetupMetabase(cli.cfg().API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDbPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -164,26 +168,28 @@ cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
 			return nil
 			return nil
 		},
 		},
 	}
 	}
-	cmd.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
-	cmd.Flags().StringVarP(&metabaseDbPath, "dir", "d", "", "Shared directory with metabase container")
-	cmd.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
-	cmd.Flags().StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
-	cmd.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
-	cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
-	//cmd.Flags().StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
-	cmd.Flags().StringVar(&metabasePassword, "password", "", "metabase password")
+
+	flags := cmd.Flags()
+	flags.BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
+	flags.StringVarP(&metabaseDbPath, "dir", "d", "", "Shared directory with metabase container")
+	flags.StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
+	flags.StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
+	flags.StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
+	flags.BoolVarP(&forceYes, "yes", "y", false, "force  yes")
+	//flags.StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
+	flags.StringVar(&metabasePassword, "password", "", "metabase password")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliDashboard) NewStartCmd() *cobra.Command {
+func (cli *cliDashboard) newStartCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "start",
 		Use:               "start",
 		Short:             "Start the metabase container.",
 		Short:             "Start the metabase container.",
 		Long:              `Stats the metabase container using docker.`,
 		Long:              `Stats the metabase container using docker.`,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(_ *cobra.Command, _ []string) error {
 			mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
 			mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
@@ -200,19 +206,20 @@ func (cli cliDashboard) NewStartCmd() *cobra.Command {
 			return nil
 			return nil
 		},
 		},
 	}
 	}
+
 	cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
 	cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliDashboard) NewStopCmd() *cobra.Command {
+func (cli *cliDashboard) newStopCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "stop",
 		Use:               "stop",
 		Short:             "Stops the metabase container.",
 		Short:             "Stops the metabase container.",
 		Long:              `Stops the metabase container using docker.`,
 		Long:              `Stops the metabase container using docker.`,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(_ *cobra.Command, _ []string) error {
 			if err := metabase.StopContainer(metabaseContainerID); err != nil {
 			if err := metabase.StopContainer(metabaseContainerID); err != nil {
 				return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
 				return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
 			}
 			}
@@ -223,12 +230,12 @@ func (cli cliDashboard) NewStopCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliDashboard) NewShowPasswordCmd() *cobra.Command {
+func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command {
 	cmd := &cobra.Command{Use: "show-password",
 	cmd := &cobra.Command{Use: "show-password",
 		Short:             "displays password of metabase.",
 		Short:             "displays password of metabase.",
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(_ *cobra.Command, _ []string) error {
 			m := metabase.Metabase{}
 			m := metabase.Metabase{}
 			if err := m.LoadConfig(metabaseConfigPath); err != nil {
 			if err := m.LoadConfig(metabaseConfigPath); err != nil {
 				return err
 				return err
@@ -241,7 +248,7 @@ func (cli cliDashboard) NewShowPasswordCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliDashboard) NewRemoveCmd() *cobra.Command {
+func (cli *cliDashboard) newRemoveCmd() *cobra.Command {
 	var force bool
 	var force bool
 
 
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
@@ -254,7 +261,7 @@ func (cli cliDashboard) NewRemoveCmd() *cobra.Command {
 cscli dashboard remove
 cscli dashboard remove
 cscli dashboard remove --force
 cscli dashboard remove --force
  `,
  `,
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: func(_ *cobra.Command, _ []string) error {
 			if !forceYes {
 			if !forceYes {
 				var answer bool
 				var answer bool
 				prompt := &survey.Confirm{
 				prompt := &survey.Confirm{
@@ -291,8 +298,8 @@ cscli dashboard remove --force
 				}
 				}
 				log.Infof("container %s stopped & removed", metabaseContainerID)
 				log.Infof("container %s stopped & removed", metabaseContainerID)
 			}
 			}
-			log.Debugf("Removing metabase db %s", csConfig.ConfigPaths.DataDir)
-			if err := metabase.RemoveDatabase(csConfig.ConfigPaths.DataDir); err != nil {
+			log.Debugf("Removing metabase db %s", cli.cfg().ConfigPaths.DataDir)
+			if err := metabase.RemoveDatabase(cli.cfg().ConfigPaths.DataDir); err != nil {
 				log.Warnf("failed to remove metabase internal db : %s", err)
 				log.Warnf("failed to remove metabase internal db : %s", err)
 			}
 			}
 			if force {
 			if force {
@@ -309,8 +316,10 @@ cscli dashboard remove --force
 			return nil
 			return nil
 		},
 		},
 	}
 	}
-	cmd.Flags().BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
-	cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force  yes")
+
+	flags := cmd.Flags()
+	flags.BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
+	flags.BoolVarP(&forceYes, "yes", "y", false, "force  yes")
 
 
 	return cmd
 	return cmd
 }
 }
@@ -431,22 +440,23 @@ func checkGroups(forceYes *bool) (*user.Group, error) {
 	return user.LookupGroup(crowdsecGroup)
 	return user.LookupGroup(crowdsecGroup)
 }
 }
 
 
-func chownDatabase(gid string) error {
+func (cli *cliDashboard) chownDatabase(gid string) error {
+	cfg := cli.cfg()
 	intID, err := strconv.Atoi(gid)
 	intID, err := strconv.Atoi(gid)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to convert group ID to int: %s", err)
 		return fmt.Errorf("unable to convert group ID to int: %s", err)
 	}
 	}
 
 
-	if stat, err := os.Stat(csConfig.DbConfig.DbPath); !os.IsNotExist(err) {
+	if stat, err := os.Stat(cfg.DbConfig.DbPath); !os.IsNotExist(err) {
 		info := stat.Sys()
 		info := stat.Sys()
-		if err := os.Chown(csConfig.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
-			return fmt.Errorf("unable to chown sqlite db file '%s': %s", csConfig.DbConfig.DbPath, err)
+		if err := os.Chown(cfg.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
+			return fmt.Errorf("unable to chown sqlite db file '%s': %s", cfg.DbConfig.DbPath, err)
 		}
 		}
 	}
 	}
 
 
-	if csConfig.DbConfig.Type == "sqlite" && csConfig.DbConfig.UseWal != nil && *csConfig.DbConfig.UseWal {
+	if cfg.DbConfig.Type == "sqlite" && cfg.DbConfig.UseWal != nil && *cfg.DbConfig.UseWal {
 		for _, ext := range []string{"-wal", "-shm"} {
 		for _, ext := range []string{"-wal", "-shm"} {
-			file := csConfig.DbConfig.DbPath + ext
+			file := cfg.DbConfig.DbPath + ext
 			if stat, err := os.Stat(file); !os.IsNotExist(err) {
 			if stat, err := os.Stat(file); !os.IsNotExist(err) {
 				info := stat.Sys()
 				info := stat.Sys()
 				if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
 				if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {

+ 8 - 4
cmd/crowdsec-cli/dashboard_unsupported.go

@@ -9,17 +9,21 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
-type cliDashboard struct{}
+type cliDashboard struct{
+	cfg configGetter
+}
 
 
-func NewCLIDashboard() *cliDashboard {
-	return &cliDashboard{}
+func NewCLIDashboard(getconfig configGetter) *cliDashboard {
+	return &cliDashboard{
+		cfg: getconfig,
+	}
 }
 }
 
 
 func (cli cliDashboard) NewCommand() *cobra.Command {
 func (cli cliDashboard) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "dashboard",
 		Use:               "dashboard",
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		Run: func(_ *cobra.Command, _ []string) {
 			log.Infof("Dashboard command is disabled on %s", runtime.GOOS)
 			log.Infof("Dashboard command is disabled on %s", runtime.GOOS)
 		},
 		},
 	}
 	}

+ 28 - 0
cmd/crowdsec-cli/flag.go

@@ -0,0 +1,28 @@
+package main
+
+// Custom types for flag validation and conversion.
+
+import (
+	"errors"
+)
+
+type MachinePassword string
+
+func (p *MachinePassword) String() string {
+    return string(*p)
+}
+
+func (p *MachinePassword) Set(v string) error {
+	// a password can't be more than 72 characters
+	// due to bcrypt limitations
+        if len(v) > 72 {
+                return errors.New("password too long (max 72 characters)")
+        }
+        *p = MachinePassword(v)
+
+        return nil
+}
+
+func (p *MachinePassword) Type() string {
+    return "string"
+}

+ 43 - 41
cmd/crowdsec-cli/hub.go

@@ -13,13 +13,17 @@ import (
 	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
 	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
 )
 )
 
 
-type cliHub struct{}
+type cliHub struct{
+	cfg configGetter
+}
 
 
-func NewCLIHub() *cliHub {
-	return &cliHub{}
+func NewCLIHub(getconfig configGetter) *cliHub {
+	return &cliHub{
+		cfg: getconfig,
+	}
 }
 }
 
 
-func (cli cliHub) NewCommand() *cobra.Command {
+func (cli *cliHub) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "hub [action]",
 		Use:   "hub [action]",
 		Short: "Manage hub index",
 		Short: "Manage hub index",
@@ -34,23 +38,16 @@ cscli hub upgrade`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 	}
 	}
 
 
-	cmd.AddCommand(cli.NewListCmd())
-	cmd.AddCommand(cli.NewUpdateCmd())
-	cmd.AddCommand(cli.NewUpgradeCmd())
-	cmd.AddCommand(cli.NewTypesCmd())
+	cmd.AddCommand(cli.newListCmd())
+	cmd.AddCommand(cli.newUpdateCmd())
+	cmd.AddCommand(cli.newUpgradeCmd())
+	cmd.AddCommand(cli.newTypesCmd())
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliHub) list(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	all, err := flags.GetBool("all")
-	if err != nil {
-		return err
-	}
-
-	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
+func (cli *cliHub) list(all bool) error {
+	hub, err := require.Hub(cli.cfg(), nil, log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -80,24 +77,28 @@ func (cli cliHub) list(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliHub) NewListCmd() *cobra.Command {
+func (cli *cliHub) newListCmd() *cobra.Command {
+	var all bool
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "list [-a]",
 		Use:               "list [-a]",
 		Short:             "List all installed configurations",
 		Short:             "List all installed configurations",
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.list,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.list(all)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.BoolP("all", "a", false, "List disabled items as well")
+	flags.BoolVarP(&all, "all", "a", false, "List disabled items as well")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliHub) update(cmd *cobra.Command, args []string) error {
-	local := csConfig.Hub
-	remote := require.RemoteHub(csConfig)
+func (cli *cliHub) update() error {
+	local := cli.cfg().Hub
+	remote := require.RemoteHub(cli.cfg())
 
 
 	// don't use require.Hub because if there is no index file, it would fail
 	// don't use require.Hub because if there is no index file, it would fail
 	hub, err := cwhub.NewHub(local, remote, true, log.StandardLogger())
 	hub, err := cwhub.NewHub(local, remote, true, log.StandardLogger())
@@ -112,7 +113,7 @@ func (cli cliHub) update(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliHub) NewUpdateCmd() *cobra.Command {
+func (cli *cliHub) newUpdateCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "update",
 		Use:   "update",
 		Short: "Download the latest index (catalog of available configurations)",
 		Short: "Download the latest index (catalog of available configurations)",
@@ -121,21 +122,16 @@ Fetches the .index.json file from the hub, containing the list of available conf
 `,
 `,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.update,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.update()
+		},
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliHub) upgrade(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	force, err := flags.GetBool("force")
-	if err != nil {
-		return err
-	}
-
-	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
+func (cli *cliHub) upgrade(force bool) error {
+	hub, err := require.Hub(cli.cfg(), require.RemoteHub(cli.cfg()), log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -167,7 +163,9 @@ func (cli cliHub) upgrade(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliHub) NewUpgradeCmd() *cobra.Command {
+func (cli *cliHub) newUpgradeCmd() *cobra.Command {
+	var force bool
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "upgrade",
 		Use:   "upgrade",
 		Short: "Upgrade all configurations to their latest version",
 		Short: "Upgrade all configurations to their latest version",
@@ -176,17 +174,19 @@ Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if
 `,
 `,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.upgrade,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.upgrade(force)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
+	flags.BoolVar(&force, "force", false, "Force upgrade: overwrite tainted and outdated files")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliHub) types(cmd *cobra.Command, args []string) error {
-	switch csConfig.Cscli.Output {
+func (cli *cliHub) types() error {
+	switch cli.cfg().Cscli.Output {
 	case "human":
 	case "human":
 		s, err := yaml.Marshal(cwhub.ItemTypes)
 		s, err := yaml.Marshal(cwhub.ItemTypes)
 		if err != nil {
 		if err != nil {
@@ -210,7 +210,7 @@ func (cli cliHub) types(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliHub) NewTypesCmd() *cobra.Command {
+func (cli *cliHub) newTypesCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "types",
 		Use:   "types",
 		Short: "List supported item types",
 		Short: "List supported item types",
@@ -219,7 +219,9 @@ List the types of supported hub items.
 `,
 `,
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.types,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.types()
+		},
 	}
 	}
 
 
 	return cmd
 	return cmd

+ 71 - 125
cmd/crowdsec-cli/itemcli.go

@@ -51,33 +51,16 @@ func (cli cliItem) NewCommand() *cobra.Command {
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 	}
 	}
 
 
-	cmd.AddCommand(cli.NewInstallCmd())
-	cmd.AddCommand(cli.NewRemoveCmd())
-	cmd.AddCommand(cli.NewUpgradeCmd())
-	cmd.AddCommand(cli.NewInspectCmd())
-	cmd.AddCommand(cli.NewListCmd())
+	cmd.AddCommand(cli.newInstallCmd())
+	cmd.AddCommand(cli.newRemoveCmd())
+	cmd.AddCommand(cli.newUpgradeCmd())
+	cmd.AddCommand(cli.newInspectCmd())
+	cmd.AddCommand(cli.newListCmd())
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliItem) Install(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	downloadOnly, err := flags.GetBool("download-only")
-	if err != nil {
-		return err
-	}
-
-	force, err := flags.GetBool("force")
-	if err != nil {
-		return err
-	}
-
-	ignoreError, err := flags.GetBool("ignore")
-	if err != nil {
-		return err
-	}
-
+func (cli cliItem) install(args []string, downloadOnly bool, force bool, ignoreError bool) error {
 	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
 	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -110,7 +93,13 @@ func (cli cliItem) Install(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliItem) NewInstallCmd() *cobra.Command {
+func (cli cliItem) newInstallCmd() *cobra.Command {
+	var (
+		downloadOnly bool
+		force        bool
+		ignoreError  bool
+	)
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               coalesce.String(cli.installHelp.use, "install [item]..."),
 		Use:               coalesce.String(cli.installHelp.use, "install [item]..."),
 		Short:             coalesce.String(cli.installHelp.short, fmt.Sprintf("Install given %s", cli.oneOrMore)),
 		Short:             coalesce.String(cli.installHelp.short, fmt.Sprintf("Install given %s", cli.oneOrMore)),
@@ -121,13 +110,15 @@ func (cli cliItem) NewInstallCmd() *cobra.Command {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compAllItems(cli.name, args, toComplete)
 			return compAllItems(cli.name, args, toComplete)
 		},
 		},
-		RunE: cli.Install,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cli.install(args, downloadOnly, force, ignoreError)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
-	flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
-	flags.Bool("ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", cli.name))
+	flags.BoolVarP(&downloadOnly, "download-only", "d", false, "Only download packages, don't enable")
+	flags.BoolVar(&force, "force", false, "Force install: overwrite tainted and outdated files")
+	flags.BoolVar(&ignoreError, "ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", cli.name))
 
 
 	return cmd
 	return cmd
 }
 }
@@ -145,24 +136,7 @@ func istalledParentNames(item *cwhub.Item) []string {
 	return ret
 	return ret
 }
 }
 
 
-func (cli cliItem) Remove(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	purge, err := flags.GetBool("purge")
-	if err != nil {
-		return err
-	}
-
-	force, err := flags.GetBool("force")
-	if err != nil {
-		return err
-	}
-
-	all, err := flags.GetBool("all")
-	if err != nil {
-		return err
-	}
-
+func (cli cliItem) remove(args []string, purge bool, force bool, all bool) error {
 	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
 	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -243,7 +217,13 @@ func (cli cliItem) Remove(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliItem) NewRemoveCmd() *cobra.Command {
+func (cli cliItem) newRemoveCmd() *cobra.Command {
+	var (
+		purge bool
+		force bool
+		all   bool
+	)
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               coalesce.String(cli.removeHelp.use, "remove [item]..."),
 		Use:               coalesce.String(cli.removeHelp.use, "remove [item]..."),
 		Short:             coalesce.String(cli.removeHelp.short, fmt.Sprintf("Remove given %s", cli.oneOrMore)),
 		Short:             coalesce.String(cli.removeHelp.short, fmt.Sprintf("Remove given %s", cli.oneOrMore)),
@@ -254,30 +234,20 @@ func (cli cliItem) NewRemoveCmd() *cobra.Command {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cli.name, args, toComplete)
 			return compInstalledItems(cli.name, args, toComplete)
 		},
 		},
-		RunE: cli.Remove,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cli.remove(args, purge, force, all)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.Bool("purge", false, "Delete source file too")
-	flags.Bool("force", false, "Force remove: remove tainted and outdated files")
-	flags.Bool("all", false, fmt.Sprintf("Remove all the %s", cli.name))
+	flags.BoolVar(&purge, "purge", false, "Delete source file too")
+	flags.BoolVar(&force, "force", false, "Force remove: remove tainted and outdated files")
+	flags.BoolVar(&all, "all", false, fmt.Sprintf("Remove all the %s", cli.name))
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliItem) Upgrade(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	force, err := flags.GetBool("force")
-	if err != nil {
-		return err
-	}
-
-	all, err := flags.GetBool("all")
-	if err != nil {
-		return err
-	}
-
+func (cli cliItem) upgrade(args []string, force bool, all bool) error {
 	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
 	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -341,7 +311,12 @@ func (cli cliItem) Upgrade(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliItem) NewUpgradeCmd() *cobra.Command {
+func (cli cliItem) newUpgradeCmd() *cobra.Command {
+	var (
+		all   bool
+		force bool
+	)
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               coalesce.String(cli.upgradeHelp.use, "upgrade [item]..."),
 		Use:               coalesce.String(cli.upgradeHelp.use, "upgrade [item]..."),
 		Short:             coalesce.String(cli.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", cli.oneOrMore)),
 		Short:             coalesce.String(cli.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", cli.oneOrMore)),
@@ -351,43 +326,27 @@ func (cli cliItem) NewUpgradeCmd() *cobra.Command {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cli.name, args, toComplete)
 			return compInstalledItems(cli.name, args, toComplete)
 		},
 		},
-		RunE: cli.Upgrade,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cli.upgrade(args, force, all)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.BoolP("all", "a", false, fmt.Sprintf("Upgrade all the %s", cli.name))
-	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
+	flags.BoolVarP(&all, "all", "a", false, fmt.Sprintf("Upgrade all the %s", cli.name))
+	flags.BoolVar(&force, "force", false, "Force upgrade: overwrite tainted and outdated files")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	url, err := flags.GetString("url")
-	if err != nil {
-		return err
+func (cli cliItem) inspect(args []string, url string, diff bool, rev bool, noMetrics bool) error {
+	if rev && !diff {
+		return fmt.Errorf("--rev can only be used with --diff")
 	}
 	}
 
 
 	if url != "" {
 	if url != "" {
 		csConfig.Cscli.PrometheusUrl = url
 		csConfig.Cscli.PrometheusUrl = url
 	}
 	}
 
 
-	diff, err := flags.GetBool("diff")
-	if err != nil {
-		return err
-	}
-
-	rev, err := flags.GetBool("rev")
-	if err != nil {
-		return err
-	}
-
-	noMetrics, err := flags.GetBool("no-metrics")
-	if err != nil {
-		return err
-	}
-
 	remote := (*cwhub.RemoteHubCfg)(nil)
 	remote := (*cwhub.RemoteHubCfg)(nil)
 
 
 	if diff {
 	if diff {
@@ -411,7 +370,7 @@ func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error {
 			continue
 			continue
 		}
 		}
 
 
-		if err = InspectItem(item, !noMetrics); err != nil {
+		if err = inspectItem(item, !noMetrics); err != nil {
 			return err
 			return err
 		}
 		}
 
 
@@ -425,7 +384,14 @@ func (cli cliItem) Inspect(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliItem) NewInspectCmd() *cobra.Command {
+func (cli cliItem) newInspectCmd() *cobra.Command {
+	var (
+		url       string
+		diff      bool
+		rev       bool
+		noMetrics bool
+	)
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               coalesce.String(cli.inspectHelp.use, "inspect [item]..."),
 		Use:               coalesce.String(cli.inspectHelp.use, "inspect [item]..."),
 		Short:             coalesce.String(cli.inspectHelp.short, fmt.Sprintf("Inspect given %s", cli.oneOrMore)),
 		Short:             coalesce.String(cli.inspectHelp.short, fmt.Sprintf("Inspect given %s", cli.oneOrMore)),
@@ -436,45 +402,21 @@ func (cli cliItem) NewInspectCmd() *cobra.Command {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cli.name, args, toComplete)
 			return compInstalledItems(cli.name, args, toComplete)
 		},
 		},
-		PreRunE: func(cmd *cobra.Command, _ []string) error {
-			flags := cmd.Flags()
-
-			diff, err := flags.GetBool("diff")
-			if err != nil {
-				return err
-			}
-
-			rev, err := flags.GetBool("rev")
-			if err != nil {
-				return err
-			}
-
-			if rev && !diff {
-				return fmt.Errorf("--rev can only be used with --diff")
-			}
-
-			return nil
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cli.inspect(args, url, diff, rev, noMetrics)
 		},
 		},
-		RunE: cli.Inspect,
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.StringP("url", "u", "", "Prometheus url")
-	flags.Bool("diff", false, "Show diff with latest version (for tainted items)")
-	flags.Bool("rev", false, "Reverse diff output")
-	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
+	flags.StringVarP(&url, "url", "u", "", "Prometheus url")
+	flags.BoolVar(&diff, "diff", false, "Show diff with latest version (for tainted items)")
+	flags.BoolVar(&rev, "rev", false, "Reverse diff output")
+	flags.BoolVar(&noMetrics, "no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliItem) List(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	all, err := flags.GetBool("all")
-	if err != nil {
-		return err
-	}
-
+func (cli cliItem) list(args []string, all bool) error {
 	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
 	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -494,18 +436,22 @@ func (cli cliItem) List(cmd *cobra.Command, args []string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliItem) NewListCmd() *cobra.Command {
+func (cli cliItem) newListCmd() *cobra.Command {
+	var all bool
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               coalesce.String(cli.listHelp.use, "list [item... | -a]"),
 		Use:               coalesce.String(cli.listHelp.use, "list [item... | -a]"),
 		Short:             coalesce.String(cli.listHelp.short, fmt.Sprintf("List %s", cli.oneOrMore)),
 		Short:             coalesce.String(cli.listHelp.short, fmt.Sprintf("List %s", cli.oneOrMore)),
 		Long:              coalesce.String(cli.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", cli.name)),
 		Long:              coalesce.String(cli.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", cli.name)),
 		Example:           cli.listHelp.example,
 		Example:           cli.listHelp.example,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE:              cli.List,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cli.list(args, all)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.BoolP("all", "a", false, "List disabled items as well")
+	flags.BoolVarP(&all, "all", "a", false, "List disabled items as well")
 
 
 	return cmd
 	return cmd
 }
 }

+ 1 - 3
cmd/crowdsec-cli/items.go

@@ -138,14 +138,12 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item
 		}
 		}
 
 
 		csvwriter.Flush()
 		csvwriter.Flush()
-	default:
-		return fmt.Errorf("unknown output format '%s'", csConfig.Cscli.Output)
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func InspectItem(item *cwhub.Item, showMetrics bool) error {
+func inspectItem(item *cwhub.Item, showMetrics bool) error {
 	switch csConfig.Cscli.Output {
 	switch csConfig.Cscli.Output {
 	case "human", "raw":
 	case "human", "raw":
 		enc := yaml.NewEncoder(os.Stdout)
 		enc := yaml.NewEncoder(os.Stdout)

+ 214 - 206
cmd/crowdsec-cli/machines.go

@@ -5,7 +5,6 @@ import (
 	"encoding/csv"
 	"encoding/csv"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"io"
 	"math/big"
 	"math/big"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -101,85 +100,97 @@ func getLastHeartbeat(m *ent.Machine) (string, bool) {
 	return hb, true
 	return hb, true
 }
 }
 
 
-func getAgents(out io.Writer, dbClient *database.Client) error {
-	machines, err := dbClient.ListMachines()
+type cliMachines struct{
+	db *database.Client
+	cfg configGetter
+}
+
+func NewCLIMachines(getconfig configGetter) *cliMachines {
+	return &cliMachines{
+		cfg: getconfig,
+	}
+}
+
+func (cli *cliMachines) NewCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "machines [action]",
+		Short: "Manage local API machines [requires local API]",
+		Long: `To list/add/delete/validate/prune machines.
+Note: This command requires database direct access, so is intended to be run on the local API machine.
+`,
+		Example:           `cscli machines [action]`,
+		DisableAutoGenTag: true,
+		Aliases:           []string{"machine"},
+		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
+			var err error
+			if err = require.LAPI(cli.cfg()); err != nil {
+				return err
+			}
+			cli.db, err = database.NewClient(cli.cfg().DbConfig)
+			if err != nil {
+				return fmt.Errorf("unable to create new database client: %s", err)
+			}
+			return nil
+		},
+	}
+
+	cmd.AddCommand(cli.newListCmd())
+	cmd.AddCommand(cli.newAddCmd())
+	cmd.AddCommand(cli.newDeleteCmd())
+	cmd.AddCommand(cli.newValidateCmd())
+	cmd.AddCommand(cli.newPruneCmd())
+
+	return cmd
+}
+
+func (cli *cliMachines) list() error {
+	out := color.Output
+
+	machines, err := cli.db.ListMachines()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to list machines: %s", err)
 		return fmt.Errorf("unable to list machines: %s", err)
 	}
 	}
 
 
-	switch csConfig.Cscli.Output {
+	switch cli.cfg().Cscli.Output {
 	case "human":
 	case "human":
 		getAgentsTable(out, machines)
 		getAgentsTable(out, machines)
 	case "json":
 	case "json":
 		enc := json.NewEncoder(out)
 		enc := json.NewEncoder(out)
 		enc.SetIndent("", "  ")
 		enc.SetIndent("", "  ")
+
 		if err := enc.Encode(machines); err != nil {
 		if err := enc.Encode(machines); err != nil {
 			return fmt.Errorf("failed to marshal")
 			return fmt.Errorf("failed to marshal")
 		}
 		}
+
 		return nil
 		return nil
 	case "raw":
 	case "raw":
 		csvwriter := csv.NewWriter(out)
 		csvwriter := csv.NewWriter(out)
+
 		err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
 		err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("failed to write header: %s", err)
 			return fmt.Errorf("failed to write header: %s", err)
 		}
 		}
+
 		for _, m := range machines {
 		for _, m := range machines {
 			validated := "false"
 			validated := "false"
 			if m.IsValidated {
 			if m.IsValidated {
 				validated = "true"
 				validated = "true"
 			}
 			}
+
 			hb, _ := getLastHeartbeat(m)
 			hb, _ := getLastHeartbeat(m)
-			err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb})
-			if err != nil {
+
+			if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb}); err != nil {
 				return fmt.Errorf("failed to write raw output: %w", err)
 				return fmt.Errorf("failed to write raw output: %w", err)
 			}
 			}
 		}
 		}
-		csvwriter.Flush()
-	default:
-		return fmt.Errorf("unknown output '%s'", csConfig.Cscli.Output)
-	}
-	return nil
-}
-
-type cliMachines struct{}
-
-func NewCLIMachines() *cliMachines {
-	return &cliMachines{}
-}
 
 
-func (cli cliMachines) NewCommand() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "machines [action]",
-		Short: "Manage local API machines [requires local API]",
-		Long: `To list/add/delete/validate/prune machines.
-Note: This command requires database direct access, so is intended to be run on the local API machine.
-`,
-		Example:           `cscli machines [action]`,
-		DisableAutoGenTag: true,
-		Aliases:           []string{"machine"},
-		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
-			var err error
-			if err = require.LAPI(csConfig); err != nil {
-				return err
-			}
-			dbClient, err = database.NewClient(csConfig.DbConfig)
-			if err != nil {
-				return fmt.Errorf("unable to create new database client: %s", err)
-			}
-			return nil
-		},
+		csvwriter.Flush()
 	}
 	}
 
 
-	cmd.AddCommand(cli.NewListCmd())
-	cmd.AddCommand(cli.NewAddCmd())
-	cmd.AddCommand(cli.NewDeleteCmd())
-	cmd.AddCommand(cli.NewValidateCmd())
-	cmd.AddCommand(cli.NewPruneCmd())
-
-	return cmd
+	return nil
 }
 }
 
 
-func (cli cliMachines) NewListCmd() *cobra.Command {
+func (cli *cliMachines) newListCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "list",
 		Use:               "list",
 		Short:             "list all machines in the database",
 		Short:             "list all machines in the database",
@@ -188,84 +199,60 @@ func (cli cliMachines) NewListCmd() *cobra.Command {
 		Args:              cobra.NoArgs,
 		Args:              cobra.NoArgs,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		RunE: func(_ *cobra.Command, _ []string) error {
 		RunE: func(_ *cobra.Command, _ []string) error {
-			err := getAgents(color.Output, dbClient)
-			if err != nil {
-				return fmt.Errorf("unable to list machines: %s", err)
-			}
-
-			return nil
+			return cli.list()
 		},
 		},
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliMachines) NewAddCmd() *cobra.Command {
+func (cli *cliMachines) newAddCmd() *cobra.Command {
+	var (
+		password    MachinePassword
+		dumpFile    string
+		apiURL      string
+		interactive bool
+		autoAdd     bool
+		force       bool
+	)
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "add",
 		Use:               "add",
 		Short:             "add a single machine to the database",
 		Short:             "add a single machine to the database",
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		Long:              `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
 		Long:              `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
-		Example: `
-cscli machines add --auto
+		Example: `cscli machines add --auto
 cscli machines add MyTestMachine --auto
 cscli machines add MyTestMachine --auto
 cscli machines add MyTestMachine --password MyPassword
 cscli machines add MyTestMachine --password MyPassword
-`,
-		RunE: cli.add,
+cscli machines add -f- --auto > /tmp/mycreds.yaml`,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cli.add(args, string(password), dumpFile, apiURL, interactive, autoAdd, force)
+		},
 	}
 	}
 
 
 	flags := cmd.Flags()
 	flags := cmd.Flags()
-	flags.StringP("password", "p", "", "machine password to login to the API")
-	flags.StringP("file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")")
-	flags.StringP("url", "u", "", "URL of the local API")
-	flags.BoolP("interactive", "i", false, "interfactive mode to enter the password")
-	flags.BoolP("auto", "a", false, "automatically generate password (and username if not provided)")
-	flags.Bool("force", false, "will force add the machine if it already exist")
+	flags.VarP(&password, "password", "p", "machine password to login to the API")
+	flags.StringVarP(&dumpFile, "file", "f", "", "output file destination (defaults to "+csconfig.DefaultConfigPath("local_api_credentials.yaml")+")")
+	flags.StringVarP(&apiURL, "url", "u", "", "URL of the local API")
+	flags.BoolVarP(&interactive, "interactive", "i", false, "interfactive mode to enter the password")
+	flags.BoolVarP(&autoAdd, "auto", "a", false, "automatically generate password (and username if not provided)")
+	flags.BoolVar(&force, "force", false, "will force add the machine if it already exist")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliMachines) add(cmd *cobra.Command, args []string) error {
-	flags := cmd.Flags()
-
-	machinePassword, err := flags.GetString("password")
-	if err != nil {
-		return err
-	}
-
-	dumpFile, err := flags.GetString("file")
-	if err != nil {
-		return err
-	}
-
-	apiURL, err := flags.GetString("url")
-	if err != nil {
-		return err
-	}
-
-	interactive, err := flags.GetBool("interactive")
-	if err != nil {
-		return err
-	}
-
-	autoAdd, err := flags.GetBool("auto")
-	if err != nil {
-		return err
-	}
-
-	force, err := flags.GetBool("force")
-	if err != nil {
-		return err
-	}
-
-	var machineID string
+func (cli *cliMachines) add(args []string, machinePassword string, dumpFile string, apiURL string, interactive bool, autoAdd bool, force bool) error {
+	var (
+		err error
+		machineID string
+	)
 
 
 	// create machineID if not specified by user
 	// create machineID if not specified by user
 	if len(args) == 0 {
 	if len(args) == 0 {
 		if !autoAdd {
 		if !autoAdd {
-			printHelp(cmd)
-			return nil
+			return fmt.Errorf("please specify a machine name to add, or use --auto")
 		}
 		}
+
 		machineID, err = generateID("")
 		machineID, err = generateID("")
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("unable to generate machine id: %s", err)
 			return fmt.Errorf("unable to generate machine id: %s", err)
@@ -274,15 +261,18 @@ func (cli cliMachines) add(cmd *cobra.Command, args []string) error {
 		machineID = args[0]
 		machineID = args[0]
 	}
 	}
 
 
+	clientCfg := cli.cfg().API.Client
+	serverCfg := cli.cfg().API.Server
+
 	/*check if file already exists*/
 	/*check if file already exists*/
-	if dumpFile == "" && csConfig.API.Client != nil && csConfig.API.Client.CredentialsFilePath != "" {
-		credFile := csConfig.API.Client.CredentialsFilePath
+	if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" {
+		credFile := clientCfg.CredentialsFilePath
 		// use the default only if the file does not exist
 		// use the default only if the file does not exist
 		_, err = os.Stat(credFile)
 		_, err = os.Stat(credFile)
 
 
 		switch {
 		switch {
 		case os.IsNotExist(err) || force:
 		case os.IsNotExist(err) || force:
-			dumpFile = csConfig.API.Client.CredentialsFilePath
+			dumpFile = credFile
 		case err != nil:
 		case err != nil:
 			return fmt.Errorf("unable to stat '%s': %s", credFile, err)
 			return fmt.Errorf("unable to stat '%s': %s", credFile, err)
 		default:
 		default:
@@ -302,49 +292,85 @@ func (cli cliMachines) add(cmd *cobra.Command, args []string) error {
 		machinePassword = generatePassword(passwordLength)
 		machinePassword = generatePassword(passwordLength)
 	} else if machinePassword == "" && interactive {
 	} else if machinePassword == "" && interactive {
 		qs := &survey.Password{
 		qs := &survey.Password{
-			Message: "Please provide a password for the machine",
+			Message: "Please provide a password for the machine:",
 		}
 		}
 		survey.AskOne(qs, &machinePassword)
 		survey.AskOne(qs, &machinePassword)
 	}
 	}
+
 	password := strfmt.Password(machinePassword)
 	password := strfmt.Password(machinePassword)
-	_, err = dbClient.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType)
+
+	_, err = cli.db.CreateMachine(&machineID, &password, "", true, force, types.PasswordAuthType)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to create machine: %s", err)
 		return fmt.Errorf("unable to create machine: %s", err)
 	}
 	}
-	fmt.Printf("Machine '%s' successfully added to the local API.\n", machineID)
+
+	fmt.Fprintf(os.Stderr, "Machine '%s' successfully added to the local API.\n", machineID)
 
 
 	if apiURL == "" {
 	if apiURL == "" {
-		if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil && csConfig.API.Client.Credentials.URL != "" {
-			apiURL = csConfig.API.Client.Credentials.URL
-		} else if csConfig.API.Server != nil && csConfig.API.Server.ListenURI != "" {
-			apiURL = "http://" + csConfig.API.Server.ListenURI
+		if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" {
+			apiURL = clientCfg.Credentials.URL
+		} else if serverCfg != nil && serverCfg.ListenURI != "" {
+			apiURL = "http://" + serverCfg.ListenURI
 		} else {
 		} else {
 			return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter")
 			return fmt.Errorf("unable to dump an api URL. Please provide it in your configuration or with the -u parameter")
 		}
 		}
 	}
 	}
+
 	apiCfg := csconfig.ApiCredentialsCfg{
 	apiCfg := csconfig.ApiCredentialsCfg{
 		Login:    machineID,
 		Login:    machineID,
 		Password: password.String(),
 		Password: password.String(),
 		URL:      apiURL,
 		URL:      apiURL,
 	}
 	}
+
 	apiConfigDump, err := yaml.Marshal(apiCfg)
 	apiConfigDump, err := yaml.Marshal(apiCfg)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to marshal api credentials: %s", err)
 		return fmt.Errorf("unable to marshal api credentials: %s", err)
 	}
 	}
+
 	if dumpFile != "" && dumpFile != "-" {
 	if dumpFile != "" && dumpFile != "-" {
 		err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
 		err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err)
 			return fmt.Errorf("write api credentials in '%s' failed: %s", dumpFile, err)
 		}
 		}
-		fmt.Printf("API credentials written to '%s'.\n", dumpFile)
+		fmt.Fprintf(os.Stderr, "API credentials written to '%s'.\n", dumpFile)
 	} else {
 	} else {
-		fmt.Printf("%s\n", string(apiConfigDump))
+		fmt.Print(string(apiConfigDump))
 	}
 	}
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliMachines) NewDeleteCmd() *cobra.Command {
+func (cli *cliMachines) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	machines, err := cli.db.ListMachines()
+	if err != nil {
+		cobra.CompError("unable to list machines " + err.Error())
+	}
+
+	ret := []string{}
+
+	for _, machine := range machines {
+		if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) {
+			ret = append(ret, machine.MachineId)
+		}
+	}
+
+	return ret, cobra.ShellCompDirectiveNoFileComp
+}
+
+func (cli *cliMachines) delete(machines []string) error {
+	for _, machineID := range machines {
+		err := cli.db.DeleteWatcher(machineID)
+		if err != nil {
+			log.Errorf("unable to delete machine '%s': %s", machineID, err)
+			return nil
+		}
+		log.Infof("machine '%s' deleted successfully", machineID)
+	}
+
+	return nil
+}
+
+func (cli *cliMachines) newDeleteCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "delete [machine_name]...",
 		Use:               "delete [machine_name]...",
 		Short:             "delete machine(s) by name",
 		Short:             "delete machine(s) by name",
@@ -352,40 +378,75 @@ func (cli cliMachines) NewDeleteCmd() *cobra.Command {
 		Args:              cobra.MinimumNArgs(1),
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"remove"},
 		Aliases:           []string{"remove"},
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-			machines, err := dbClient.ListMachines()
-			if err != nil {
-				cobra.CompError("unable to list machines " + err.Error())
-			}
-			ret := make([]string, 0)
-			for _, machine := range machines {
-				if strings.Contains(machine.MachineId, toComplete) && !slices.Contains(args, machine.MachineId) {
-					ret = append(ret, machine.MachineId)
-				}
-			}
-			return ret, cobra.ShellCompDirectiveNoFileComp
+		ValidArgsFunction: cli.deleteValid,
+		RunE: func(_ *cobra.Command, args []string) error {
+			return cli.delete(args)
 		},
 		},
-		RunE: cli.delete,
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliMachines) delete(_ *cobra.Command, args []string) error {
-	for _, machineID := range args {
-		err := dbClient.DeleteWatcher(machineID)
-		if err != nil {
-			log.Errorf("unable to delete machine '%s': %s", machineID, err)
+func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force bool) error {
+	if duration < 2*time.Minute && !notValidOnly {
+		if yes, err := askYesNo(
+				"The duration you provided is less than 2 minutes. " +
+				"This can break installations if the machines are only temporarily disconnected. Continue?", false); err != nil {
+			return err
+		} else if !yes {
+			fmt.Println("User aborted prune. No changes were made.")
 			return nil
 			return nil
 		}
 		}
-		log.Infof("machine '%s' deleted successfully", machineID)
 	}
 	}
 
 
+	machines := []*ent.Machine{}
+	if pending, err := cli.db.QueryPendingMachine(); err == nil {
+		machines = append(machines, pending...)
+	}
+
+	if !notValidOnly {
+		if pending, err := cli.db.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(duration)); err == nil {
+			machines = append(machines, pending...)
+		}
+	}
+
+	if len(machines) == 0 {
+		fmt.Println("no machines to prune")
+		return nil
+	}
+
+	getAgentsTable(color.Output, machines)
+
+	if !force {
+		if yes, err := askYesNo(
+				"You are about to PERMANENTLY remove the above machines from the database. " +
+				"These will NOT be recoverable. Continue?", false); err != nil {
+			return err
+		} else if !yes {
+			fmt.Println("User aborted prune. No changes were made.")
+			return nil
+		}
+	}
+
+	deleted, err := cli.db.BulkDeleteWatchers(machines)
+	if err != nil {
+		return fmt.Errorf("unable to prune machines: %s", err)
+	}
+
+	fmt.Fprintf(os.Stderr, "successfully delete %d machines\n", deleted)
+
 	return nil
 	return nil
 }
 }
 
 
-func (cli cliMachines) NewPruneCmd() *cobra.Command {
-	var parsedDuration time.Duration
+func (cli *cliMachines) newPruneCmd() *cobra.Command {
+	var (
+		duration       time.Duration
+		notValidOnly   bool
+		force          bool
+	)
+
+	const defaultDuration = 10 * time.Minute
+
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "prune",
 		Use:   "prune",
 		Short: "prune multiple machines from the database",
 		Short: "prune multiple machines from the database",
@@ -395,76 +456,29 @@ cscli machines prune --duration 1h
 cscli machines prune --not-validated-only --force`,
 cscli machines prune --not-validated-only --force`,
 		Args:              cobra.NoArgs,
 		Args:              cobra.NoArgs,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		PreRunE: func(cmd *cobra.Command, _ []string) error {
-			dur, _ := cmd.Flags().GetString("duration")
-			var err error
-			parsedDuration, err = time.ParseDuration(fmt.Sprintf("-%s", dur))
-			if err != nil {
-				return fmt.Errorf("unable to parse duration '%s': %s", dur, err)
-			}
-			return nil
-		},
-		RunE: func(cmd *cobra.Command, _ []string) error {
-			notValidOnly, _ := cmd.Flags().GetBool("not-validated-only")
-			force, _ := cmd.Flags().GetBool("force")
-			if parsedDuration >= 0-60*time.Second && !notValidOnly {
-				var answer bool
-				prompt := &survey.Confirm{
-					Message: "The duration you provided is less than or equal 60 seconds this can break installations do you want to continue ?",
-					Default: false,
-				}
-				if err := survey.AskOne(prompt, &answer); err != nil {
-					return fmt.Errorf("unable to ask about prune check: %s", err)
-				}
-				if !answer {
-					fmt.Println("user aborted prune no changes were made")
-					return nil
-				}
-			}
-			machines := make([]*ent.Machine, 0)
-			if pending, err := dbClient.QueryPendingMachine(); err == nil {
-				machines = append(machines, pending...)
-			}
-			if !notValidOnly {
-				if pending, err := dbClient.QueryLastValidatedHeartbeatLT(time.Now().UTC().Add(parsedDuration)); err == nil {
-					machines = append(machines, pending...)
-				}
-			}
-			if len(machines) == 0 {
-				fmt.Println("no machines to prune")
-				return nil
-			}
-			getAgentsTable(color.Output, machines)
-			if !force {
-				var answer bool
-				prompt := &survey.Confirm{
-					Message: "You are about to PERMANENTLY remove the above machines from the database these will NOT be recoverable, continue ?",
-					Default: false,
-				}
-				if err := survey.AskOne(prompt, &answer); err != nil {
-					return fmt.Errorf("unable to ask about prune check: %s", err)
-				}
-				if !answer {
-					fmt.Println("user aborted prune no changes were made")
-					return nil
-				}
-			}
-			nbDeleted, err := dbClient.BulkDeleteWatchers(machines)
-			if err != nil {
-				return fmt.Errorf("unable to prune machines: %s", err)
-			}
-			fmt.Printf("successfully delete %d machines\n", nbDeleted)
-			return nil
+		RunE: func(_ *cobra.Command, _ []string) error {
+			return cli.prune(duration, notValidOnly, force)
 		},
 		},
 	}
 	}
-	cmd.Flags().StringP("duration", "d", "10m", "duration of time since validated machine last heartbeat")
-	cmd.Flags().Bool("not-validated-only", false, "only prune machines that are not validated")
-	cmd.Flags().Bool("force", false, "force prune without asking for confirmation")
+
+	flags := cmd.Flags()
+	flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat")
+	flags.BoolVar(&notValidOnly, "not-validated-only", false, "only prune machines that are not validated")
+	flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliMachines) NewValidateCmd() *cobra.Command {
+func (cli *cliMachines) validate(machineID string) error {
+	if err := cli.db.ValidateMachine(machineID); err != nil {
+		return fmt.Errorf("unable to validate machine '%s': %s", machineID, err)
+	}
+	log.Infof("machine '%s' validated successfully", machineID)
+
+	return nil
+}
+
+func (cli *cliMachines) newValidateCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "validate",
 		Use:               "validate",
 		Short:             "validate a machine to access the local API",
 		Short:             "validate a machine to access the local API",
@@ -472,14 +486,8 @@ func (cli cliMachines) NewValidateCmd() *cobra.Command {
 		Example:           `cscli machines validate "machine_name"`,
 		Example:           `cscli machines validate "machine_name"`,
 		Args:              cobra.ExactArgs(1),
 		Args:              cobra.ExactArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		RunE: func(_ *cobra.Command, args []string) error {
-			machineID := args[0]
-			if err := dbClient.ValidateMachine(machineID); err != nil {
-				return fmt.Errorf("unable to validate machine '%s': %s", machineID, err)
-			}
-			log.Infof("machine '%s' validated successfully", machineID)
-
-			return nil
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cli.validate(args[0])
 		},
 		},
 	}
 	}
 
 

+ 23 - 18
cmd/crowdsec-cli/main.go

@@ -21,7 +21,7 @@ var ConfigFilePath string
 var csConfig *csconfig.Config
 var csConfig *csconfig.Config
 var dbClient *database.Client
 var dbClient *database.Client
 
 
-var OutputFormat string
+var outputFormat string
 var OutputColor string
 var OutputColor string
 
 
 var mergedConfig string
 var mergedConfig string
@@ -29,6 +29,8 @@ var mergedConfig string
 // flagBranch overrides the value in csConfig.Cscli.HubBranch
 // flagBranch overrides the value in csConfig.Cscli.HubBranch
 var flagBranch = ""
 var flagBranch = ""
 
 
+type configGetter func() *csconfig.Config
+
 func initConfig() {
 func initConfig() {
 	var err error
 	var err error
 
 
@@ -64,16 +66,18 @@ func initConfig() {
 		csConfig.Cscli.HubBranch = flagBranch
 		csConfig.Cscli.HubBranch = flagBranch
 	}
 	}
 
 
-	if OutputFormat != "" {
-		csConfig.Cscli.Output = OutputFormat
-
-		if OutputFormat != "json" && OutputFormat != "raw" && OutputFormat != "human" {
-			log.Fatalf("output format %s unknown", OutputFormat)
-		}
+	if outputFormat != "" {
+		csConfig.Cscli.Output = outputFormat
 	}
 	}
+
 	if csConfig.Cscli.Output == "" {
 	if csConfig.Cscli.Output == "" {
 		csConfig.Cscli.Output = "human"
 		csConfig.Cscli.Output = "human"
 	}
 	}
+
+	if csConfig.Cscli.Output != "human" && csConfig.Cscli.Output != "json" && csConfig.Cscli.Output != "raw" {
+		log.Fatalf("output format '%s' not supported: must be one of human, json, raw", csConfig.Cscli.Output)
+	}
+
 	if csConfig.Cscli.Output == "json" {
 	if csConfig.Cscli.Output == "json" {
 		log.SetFormatter(&log.JSONFormatter{})
 		log.SetFormatter(&log.JSONFormatter{})
 		log.SetLevel(log.ErrorLevel)
 		log.SetLevel(log.ErrorLevel)
@@ -146,7 +150,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
 	cmd.SetOut(color.Output)
 	cmd.SetOut(color.Output)
 
 
 	cmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file")
 	cmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file")
-	cmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", "", "Output format: human, json, raw")
+	cmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "", "Output format: human, json, raw")
 	cmd.PersistentFlags().StringVarP(&OutputColor, "color", "", "auto", "Output color: yes, no, auto")
 	cmd.PersistentFlags().StringVarP(&OutputColor, "color", "", "auto", "Output color: yes, no, auto")
 	cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug")
 	cmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug")
 	cmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info")
 	cmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info")
@@ -182,17 +186,22 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
 	cmd.Flags().SortFlags = false
 	cmd.Flags().SortFlags = false
 	cmd.PersistentFlags().SortFlags = false
 	cmd.PersistentFlags().SortFlags = false
 
 
+	// we use a getter because the config is not initialized until the Execute() call
+	getconfig := func() *csconfig.Config {
+		return csConfig
+	}
+
 	cmd.AddCommand(NewCLIDoc().NewCommand(cmd))
 	cmd.AddCommand(NewCLIDoc().NewCommand(cmd))
 	cmd.AddCommand(NewCLIVersion().NewCommand())
 	cmd.AddCommand(NewCLIVersion().NewCommand())
 	cmd.AddCommand(NewConfigCmd())
 	cmd.AddCommand(NewConfigCmd())
-	cmd.AddCommand(NewCLIHub().NewCommand())
+	cmd.AddCommand(NewCLIHub(getconfig).NewCommand())
 	cmd.AddCommand(NewMetricsCmd())
 	cmd.AddCommand(NewMetricsCmd())
-	cmd.AddCommand(NewCLIDashboard().NewCommand())
+	cmd.AddCommand(NewCLIDashboard(getconfig).NewCommand())
 	cmd.AddCommand(NewCLIDecisions().NewCommand())
 	cmd.AddCommand(NewCLIDecisions().NewCommand())
 	cmd.AddCommand(NewCLIAlerts().NewCommand())
 	cmd.AddCommand(NewCLIAlerts().NewCommand())
-	cmd.AddCommand(NewCLISimulation().NewCommand())
-	cmd.AddCommand(NewCLIBouncers().NewCommand())
-	cmd.AddCommand(NewCLIMachines().NewCommand())
+	cmd.AddCommand(NewCLISimulation(getconfig).NewCommand())
+	cmd.AddCommand(NewCLIBouncers(getconfig).NewCommand())
+	cmd.AddCommand(NewCLIMachines(getconfig).NewCommand())
 	cmd.AddCommand(NewCLICapi().NewCommand())
 	cmd.AddCommand(NewCLICapi().NewCommand())
 	cmd.AddCommand(NewLapiCmd())
 	cmd.AddCommand(NewLapiCmd())
 	cmd.AddCommand(NewCompletionCmd())
 	cmd.AddCommand(NewCompletionCmd())
@@ -201,7 +210,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
 	cmd.AddCommand(NewCLIHubTest().NewCommand())
 	cmd.AddCommand(NewCLIHubTest().NewCommand())
 	cmd.AddCommand(NewCLINotifications().NewCommand())
 	cmd.AddCommand(NewCLINotifications().NewCommand())
 	cmd.AddCommand(NewCLISupport().NewCommand())
 	cmd.AddCommand(NewCLISupport().NewCommand())
-	cmd.AddCommand(NewCLIPapi().NewCommand())
+	cmd.AddCommand(NewCLIPapi(getconfig).NewCommand())
 	cmd.AddCommand(NewCLICollection().NewCommand())
 	cmd.AddCommand(NewCLICollection().NewCommand())
 	cmd.AddCommand(NewCLIParser().NewCommand())
 	cmd.AddCommand(NewCLIParser().NewCommand())
 	cmd.AddCommand(NewCLIScenario().NewCommand())
 	cmd.AddCommand(NewCLIScenario().NewCommand())
@@ -214,10 +223,6 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
 		cmd.AddCommand(NewSetupCmd())
 		cmd.AddCommand(NewSetupCmd())
 	}
 	}
 
 
-	if fflag.PapiClient.IsEnabled() {
-		cmd.AddCommand(NewCLIPapi().NewCommand())
-	}
-
 	if err := cmd.Execute(); err != nil {
 	if err := cmd.Execute(); err != nil {
 		log.Fatal(err)
 		log.Fatal(err)
 	}
 	}

+ 39 - 28
cmd/crowdsec-cli/papi.go

@@ -1,6 +1,7 @@
 package main
 package main
 
 
 import (
 import (
+	"fmt"
 	"time"
 	"time"
 
 
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
@@ -15,26 +16,31 @@ import (
 	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
 	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
 )
 )
 
 
-type cliPapi struct {}
+type cliPapi struct {
+	cfg configGetter
+}
 
 
-func NewCLIPapi() *cliPapi {
-	return &cliPapi{}
+func NewCLIPapi(getconfig configGetter) *cliPapi {
+	return &cliPapi{
+		cfg: getconfig,
+	}
 }
 }
 
 
-func (cli cliPapi) NewCommand() *cobra.Command {
-	var cmd = &cobra.Command{
+func (cli *cliPapi) NewCommand() *cobra.Command {
+	cmd := &cobra.Command{
 		Use:               "papi [action]",
 		Use:               "papi [action]",
 		Short:             "Manage interaction with Polling API (PAPI)",
 		Short:             "Manage interaction with Polling API (PAPI)",
 		Args:              cobra.MinimumNArgs(1),
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			if err := require.LAPI(csConfig); err != nil {
+			cfg := cli.cfg()
+			if err := require.LAPI(cfg); err != nil {
 				return err
 				return err
 			}
 			}
-			if err := require.CAPI(csConfig); err != nil {
+			if err := require.CAPI(cfg); err != nil {
 				return err
 				return err
 			}
 			}
-			if err := require.PAPI(csConfig); err != nil {
+			if err := require.PAPI(cfg); err != nil {
 				return err
 				return err
 			}
 			}
 			return nil
 			return nil
@@ -47,35 +53,36 @@ func (cli cliPapi) NewCommand() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliPapi) NewStatusCmd() *cobra.Command {
+func (cli *cliPapi) NewStatusCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "status",
 		Use:               "status",
 		Short:             "Get status of the Polling API",
 		Short:             "Get status of the Polling API",
 		Args:              cobra.MinimumNArgs(0),
 		Args:              cobra.MinimumNArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			var err error
 			var err error
-			dbClient, err = database.NewClient(csConfig.DbConfig)
+			cfg := cli.cfg()
+			dbClient, err = database.NewClient(cfg.DbConfig)
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize database client : %s", err)
+				return fmt.Errorf("unable to initialize database client: %s", err)
 			}
 			}
 
 
-			apic, err := apiserver.NewAPIC(csConfig.API.Server.OnlineClient, dbClient, csConfig.API.Server.ConsoleConfig, csConfig.API.Server.CapiWhitelists)
+			apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, dbClient, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
 
 
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize API client : %s", err)
+				return fmt.Errorf("unable to initialize API client: %s", err)
 			}
 			}
 
 
-			papi, err := apiserver.NewPAPI(apic, dbClient, csConfig.API.Server.ConsoleConfig, log.GetLevel())
+			papi, err := apiserver.NewPAPI(apic, dbClient, cfg.API.Server.ConsoleConfig, log.GetLevel())
 
 
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize PAPI client : %s", err)
+				return fmt.Errorf("unable to initialize PAPI client: %s", err)
 			}
 			}
 
 
 			perms, err := papi.GetPermissions()
 			perms, err := papi.GetPermissions()
 
 
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to get PAPI permissions: %s", err)
+				return fmt.Errorf("unable to get PAPI permissions: %s", err)
 			}
 			}
 			var lastTimestampStr *string
 			var lastTimestampStr *string
 			lastTimestampStr, err = dbClient.GetConfigItem(apiserver.PapiPullKey)
 			lastTimestampStr, err = dbClient.GetConfigItem(apiserver.PapiPullKey)
@@ -90,45 +97,48 @@ func (cli cliPapi) NewStatusCmd() *cobra.Command {
 			for _, sub := range perms.Categories {
 			for _, sub := range perms.Categories {
 				log.Infof(" - %s", sub)
 				log.Infof(" - %s", sub)
 			}
 			}
+
+			return nil
 		},
 		},
 	}
 	}
 
 
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliPapi) NewSyncCmd() *cobra.Command {
+func (cli *cliPapi) NewSyncCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "sync",
 		Use:               "sync",
 		Short:             "Sync with the Polling API, pulling all non-expired orders for the instance",
 		Short:             "Sync with the Polling API, pulling all non-expired orders for the instance",
 		Args:              cobra.MinimumNArgs(0),
 		Args:              cobra.MinimumNArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			var err error
 			var err error
+			cfg := cli.cfg()
 			t := tomb.Tomb{}
 			t := tomb.Tomb{}
-			dbClient, err = database.NewClient(csConfig.DbConfig)
+
+			dbClient, err = database.NewClient(cfg.DbConfig)
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize database client : %s", err)
+				return fmt.Errorf("unable to initialize database client: %s", err)
 			}
 			}
 
 
-			apic, err := apiserver.NewAPIC(csConfig.API.Server.OnlineClient, dbClient, csConfig.API.Server.ConsoleConfig, csConfig.API.Server.CapiWhitelists)
-
+			apic, err := apiserver.NewAPIC(cfg.API.Server.OnlineClient, dbClient, cfg.API.Server.ConsoleConfig, cfg.API.Server.CapiWhitelists)
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize API client : %s", err)
+				return fmt.Errorf("unable to initialize API client: %s", err)
 			}
 			}
 
 
 			t.Go(apic.Push)
 			t.Go(apic.Push)
 
 
-			papi, err := apiserver.NewPAPI(apic, dbClient, csConfig.API.Server.ConsoleConfig, log.GetLevel())
-
+			papi, err := apiserver.NewPAPI(apic, dbClient, cfg.API.Server.ConsoleConfig, log.GetLevel())
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to initialize PAPI client : %s", err)
+				return fmt.Errorf("unable to initialize PAPI client: %s", err)
 			}
 			}
+
 			t.Go(papi.SyncDecisions)
 			t.Go(papi.SyncDecisions)
 
 
 			err = papi.PullOnce(time.Time{}, true)
 			err = papi.PullOnce(time.Time{}, true)
 
 
 			if err != nil {
 			if err != nil {
-				log.Fatalf("unable to sync decisions: %s", err)
+				return fmt.Errorf("unable to sync decisions: %s", err)
 			}
 			}
 
 
 			log.Infof("Sending acknowledgements to CAPI")
 			log.Infof("Sending acknowledgements to CAPI")
@@ -138,6 +148,7 @@ func (cli cliPapi) NewSyncCmd() *cobra.Command {
 			t.Wait()
 			t.Wait()
 			time.Sleep(5 * time.Second) //FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done
 			time.Sleep(5 * time.Second) //FIXME: the push done by apic.Push is run inside a sub goroutine, sleep to make sure it's done
 
 
+			return nil
 		},
 		},
 	}
 	}
 
 

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

@@ -13,13 +13,17 @@ import (
 	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
 	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
 )
 )
 
 
-type cliSimulation struct{}
+type cliSimulation struct{
+	cfg configGetter
+}
 
 
-func NewCLISimulation() *cliSimulation {
-	return &cliSimulation{}
+func NewCLISimulation(getconfig configGetter) *cliSimulation {
+	return &cliSimulation{
+		cfg: getconfig,
+	}
 }
 }
 
 
-func (cli cliSimulation) NewCommand() *cobra.Command {
+func (cli *cliSimulation) NewCommand() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:   "simulation [command]",
 		Use:   "simulation [command]",
 		Short: "Manage simulation status of scenarios",
 		Short: "Manage simulation status of scenarios",
@@ -27,16 +31,16 @@ func (cli cliSimulation) NewCommand() *cobra.Command {
 cscli simulation enable crowdsecurity/ssh-bf
 cscli simulation enable crowdsecurity/ssh-bf
 cscli simulation disable crowdsecurity/ssh-bf`,
 cscli simulation disable crowdsecurity/ssh-bf`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			if err := csConfig.LoadSimulation(); err != nil {
-				log.Fatal(err)
+		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
+			if err := cli.cfg().LoadSimulation(); err != nil {
+				return err
 			}
 			}
-			if csConfig.Cscli.SimulationConfig == nil {
+			if cli.cfg().Cscli.SimulationConfig == nil {
 				return fmt.Errorf("no simulation configured")
 				return fmt.Errorf("no simulation configured")
 			}
 			}
 			return nil
 			return nil
 		},
 		},
-		PersistentPostRun: func(cmd *cobra.Command, args []string) {
+		PersistentPostRun: func(cmd *cobra.Command, _ []string) {
 			if cmd.Name() != "status" {
 			if cmd.Name() != "status" {
 				log.Infof(ReloadMessage())
 				log.Infof(ReloadMessage())
 			}
 			}
@@ -52,7 +56,7 @@ cscli simulation disable crowdsecurity/ssh-bf`,
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliSimulation) NewEnableCmd() *cobra.Command {
+func (cli *cliSimulation) NewEnableCmd() *cobra.Command {
 	var forceGlobalSimulation bool
 	var forceGlobalSimulation bool
 
 
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
@@ -60,10 +64,10 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
 		Short:             "Enable the simulation, globally or on specified scenarios",
 		Short:             "Enable the simulation, globally or on specified scenarios",
 		Example:           `cscli simulation enable`,
 		Example:           `cscli simulation enable`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
-			hub, err := require.Hub(csConfig, nil, nil)
+		RunE: func(cmd *cobra.Command, args []string) error {
+			hub, err := require.Hub(cli.cfg(), nil, nil)
 			if err != nil {
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 			}
 
 
 			if len(args) > 0 {
 			if len(args) > 0 {
@@ -76,37 +80,35 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
 					if !item.State.Installed {
 					if !item.State.Installed {
 						log.Warningf("'%s' isn't enabled", scenario)
 						log.Warningf("'%s' isn't enabled", scenario)
 					}
 					}
-					isExcluded := slices.Contains(csConfig.Cscli.SimulationConfig.Exclusions, scenario)
-					if *csConfig.Cscli.SimulationConfig.Simulation && !isExcluded {
+					isExcluded := slices.Contains(cli.cfg().Cscli.SimulationConfig.Exclusions, scenario)
+					if *cli.cfg().Cscli.SimulationConfig.Simulation && !isExcluded {
 						log.Warning("global simulation is already enabled")
 						log.Warning("global simulation is already enabled")
 						continue
 						continue
 					}
 					}
-					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
+					if !*cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
 						log.Warningf("simulation for '%s' already enabled", scenario)
 						log.Warningf("simulation for '%s' already enabled", scenario)
 						continue
 						continue
 					}
 					}
-					if *csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
-						if err := removeFromExclusion(scenario); err != nil {
-							log.Fatal(err)
-						}
+					if *cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
+						cli.removeFromExclusion(scenario)
 						log.Printf("simulation enabled for '%s'", scenario)
 						log.Printf("simulation enabled for '%s'", scenario)
 						continue
 						continue
 					}
 					}
-					if err := addToExclusion(scenario); err != nil {
-						log.Fatal(err)
-					}
+					cli.addToExclusion(scenario)
 					log.Printf("simulation mode for '%s' enabled", scenario)
 					log.Printf("simulation mode for '%s' enabled", scenario)
 				}
 				}
-				if err := dumpSimulationFile(); err != nil {
-					log.Fatalf("simulation enable: %s", err)
+				if err := cli.dumpSimulationFile(); err != nil {
+					return fmt.Errorf("simulation enable: %s", err)
 				}
 				}
 			} else if forceGlobalSimulation {
 			} else if forceGlobalSimulation {
-				if err := enableGlobalSimulation(); err != nil {
-					log.Fatalf("unable to enable global simulation mode : %s", err)
+				if err := cli.enableGlobalSimulation(); err != nil {
+					return fmt.Errorf("unable to enable global simulation mode: %s", err)
 				}
 				}
 			} else {
 			} else {
 				printHelp(cmd)
 				printHelp(cmd)
 			}
 			}
+
+			return nil
 		},
 		},
 	}
 	}
 	cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Enable global simulation (reverse mode)")
 	cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Enable global simulation (reverse mode)")
@@ -114,7 +116,7 @@ func (cli cliSimulation) NewEnableCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliSimulation) NewDisableCmd() *cobra.Command {
+func (cli *cliSimulation) NewDisableCmd() *cobra.Command {
 	var forceGlobalSimulation bool
 	var forceGlobalSimulation bool
 
 
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
@@ -122,18 +124,16 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
 		Short:             "Disable the simulation mode. Disable only specified scenarios",
 		Short:             "Disable the simulation mode. Disable only specified scenarios",
 		Example:           `cscli simulation disable`,
 		Example:           `cscli simulation disable`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) > 0 {
 			if len(args) > 0 {
 				for _, scenario := range args {
 				for _, scenario := range args {
-					isExcluded := slices.Contains(csConfig.Cscli.SimulationConfig.Exclusions, scenario)
-					if !*csConfig.Cscli.SimulationConfig.Simulation && !isExcluded {
+					isExcluded := slices.Contains(cli.cfg().Cscli.SimulationConfig.Exclusions, scenario)
+					if !*cli.cfg().Cscli.SimulationConfig.Simulation && !isExcluded {
 						log.Warningf("%s isn't in simulation mode", scenario)
 						log.Warningf("%s isn't in simulation mode", scenario)
 						continue
 						continue
 					}
 					}
-					if !*csConfig.Cscli.SimulationConfig.Simulation && isExcluded {
-						if err := removeFromExclusion(scenario); err != nil {
-							log.Fatal(err)
-						}
+					if !*cli.cfg().Cscli.SimulationConfig.Simulation && isExcluded {
+						cli.removeFromExclusion(scenario)
 						log.Printf("simulation mode for '%s' disabled", scenario)
 						log.Printf("simulation mode for '%s' disabled", scenario)
 						continue
 						continue
 					}
 					}
@@ -141,21 +141,21 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
 						log.Warningf("simulation mode is enabled but is already disable for '%s'", scenario)
 						log.Warningf("simulation mode is enabled but is already disable for '%s'", scenario)
 						continue
 						continue
 					}
 					}
-					if err := addToExclusion(scenario); err != nil {
-						log.Fatal(err)
-					}
+					cli.addToExclusion(scenario)
 					log.Printf("simulation mode for '%s' disabled", scenario)
 					log.Printf("simulation mode for '%s' disabled", scenario)
 				}
 				}
-				if err := dumpSimulationFile(); err != nil {
-					log.Fatalf("simulation disable: %s", err)
+				if err := cli.dumpSimulationFile(); err != nil {
+					return fmt.Errorf("simulation disable: %s", err)
 				}
 				}
 			} else if forceGlobalSimulation {
 			} else if forceGlobalSimulation {
-				if err := disableGlobalSimulation(); err != nil {
-					log.Fatalf("unable to disable global simulation mode : %s", err)
+				if err := cli.disableGlobalSimulation(); err != nil {
+					return fmt.Errorf("unable to disable global simulation mode: %s", err)
 				}
 				}
 			} else {
 			} else {
 				printHelp(cmd)
 				printHelp(cmd)
 			}
 			}
+
+			return nil
 		},
 		},
 	}
 	}
 	cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Disable global simulation (reverse mode)")
 	cmd.Flags().BoolVarP(&forceGlobalSimulation, "global", "g", false, "Disable global simulation (reverse mode)")
@@ -163,16 +163,14 @@ func (cli cliSimulation) NewDisableCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func (cli cliSimulation) NewStatusCmd() *cobra.Command {
+func (cli *cliSimulation) NewStatusCmd() *cobra.Command {
 	cmd := &cobra.Command{
 	cmd := &cobra.Command{
 		Use:               "status",
 		Use:               "status",
 		Short:             "Show simulation mode status",
 		Short:             "Show simulation mode status",
 		Example:           `cscli simulation status`,
 		Example:           `cscli simulation status`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
-		Run: func(cmd *cobra.Command, args []string) {
-			if err := simulationStatus(); err != nil {
-				log.Fatal(err)
-			}
+		Run: func(_ *cobra.Command, _ []string) {
+			cli.status()
 		},
 		},
 		PersistentPostRun: func(cmd *cobra.Command, args []string) {
 		PersistentPostRun: func(cmd *cobra.Command, args []string) {
 		},
 		},
@@ -181,29 +179,29 @@ func (cli cliSimulation) NewStatusCmd() *cobra.Command {
 	return cmd
 	return cmd
 }
 }
 
 
-func addToExclusion(name string) error {
-	csConfig.Cscli.SimulationConfig.Exclusions = append(csConfig.Cscli.SimulationConfig.Exclusions, name)
-	return nil
+func (cli *cliSimulation) addToExclusion(name string) {
+	cfg := cli.cfg()
+	cfg.Cscli.SimulationConfig.Exclusions = append(cfg.Cscli.SimulationConfig.Exclusions, name)
 }
 }
 
 
-func removeFromExclusion(name string) error {
-	index := slices.Index(csConfig.Cscli.SimulationConfig.Exclusions, name)
+func (cli *cliSimulation) removeFromExclusion(name string) {
+	cfg := cli.cfg()
+	index := slices.Index(cfg.Cscli.SimulationConfig.Exclusions, name)
 
 
 	// Remove element from the slice
 	// Remove element from the slice
-	csConfig.Cscli.SimulationConfig.Exclusions[index] = csConfig.Cscli.SimulationConfig.Exclusions[len(csConfig.Cscli.SimulationConfig.Exclusions)-1]
-	csConfig.Cscli.SimulationConfig.Exclusions[len(csConfig.Cscli.SimulationConfig.Exclusions)-1] = ""
-	csConfig.Cscli.SimulationConfig.Exclusions = csConfig.Cscli.SimulationConfig.Exclusions[:len(csConfig.Cscli.SimulationConfig.Exclusions)-1]
-
-	return nil
+	cfg.Cscli.SimulationConfig.Exclusions[index] = cfg.Cscli.SimulationConfig.Exclusions[len(cfg.Cscli.SimulationConfig.Exclusions)-1]
+	cfg.Cscli.SimulationConfig.Exclusions[len(cfg.Cscli.SimulationConfig.Exclusions)-1] = ""
+	cfg.Cscli.SimulationConfig.Exclusions = cfg.Cscli.SimulationConfig.Exclusions[:len(cfg.Cscli.SimulationConfig.Exclusions)-1]
 }
 }
 
 
-func enableGlobalSimulation() error {
-	csConfig.Cscli.SimulationConfig.Simulation = new(bool)
-	*csConfig.Cscli.SimulationConfig.Simulation = true
-	csConfig.Cscli.SimulationConfig.Exclusions = []string{}
+func (cli *cliSimulation) enableGlobalSimulation() error {
+	cfg := cli.cfg()
+	cfg.Cscli.SimulationConfig.Simulation = new(bool)
+	*cfg.Cscli.SimulationConfig.Simulation = true
+	cfg.Cscli.SimulationConfig.Exclusions = []string{}
 
 
-	if err := dumpSimulationFile(); err != nil {
-		log.Fatalf("unable to dump simulation file: %s", err)
+	if err := cli.dumpSimulationFile(); err != nil {
+		return fmt.Errorf("unable to dump simulation file: %s", err)
 	}
 	}
 
 
 	log.Printf("global simulation: enabled")
 	log.Printf("global simulation: enabled")
@@ -211,59 +209,61 @@ func enableGlobalSimulation() error {
 	return nil
 	return nil
 }
 }
 
 
-func dumpSimulationFile() error {
-	newConfigSim, err := yaml.Marshal(csConfig.Cscli.SimulationConfig)
+func (cli *cliSimulation) dumpSimulationFile() error {
+	cfg := cli.cfg()
+	newConfigSim, err := yaml.Marshal(cfg.Cscli.SimulationConfig)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to marshal simulation configuration: %s", err)
 		return fmt.Errorf("unable to marshal simulation configuration: %s", err)
 	}
 	}
-	err = os.WriteFile(csConfig.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
+	err = os.WriteFile(cfg.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("write simulation config in '%s' failed: %s", csConfig.ConfigPaths.SimulationFilePath, err)
+		return fmt.Errorf("write simulation config in '%s' failed: %s", cfg.ConfigPaths.SimulationFilePath, err)
 	}
 	}
-	log.Debugf("updated simulation file %s", csConfig.ConfigPaths.SimulationFilePath)
+	log.Debugf("updated simulation file %s", cfg.ConfigPaths.SimulationFilePath)
 
 
 	return nil
 	return nil
 }
 }
 
 
-func disableGlobalSimulation() error {
-	csConfig.Cscli.SimulationConfig.Simulation = new(bool)
-	*csConfig.Cscli.SimulationConfig.Simulation = false
+func (cli *cliSimulation) disableGlobalSimulation() error {
+	cfg := cli.cfg()
+	cfg.Cscli.SimulationConfig.Simulation = new(bool)
+	*cfg.Cscli.SimulationConfig.Simulation = false
 
 
-	csConfig.Cscli.SimulationConfig.Exclusions = []string{}
-	newConfigSim, err := yaml.Marshal(csConfig.Cscli.SimulationConfig)
+	cfg.Cscli.SimulationConfig.Exclusions = []string{}
+	newConfigSim, err := yaml.Marshal(cfg.Cscli.SimulationConfig)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("unable to marshal new simulation configuration: %s", err)
 		return fmt.Errorf("unable to marshal new simulation configuration: %s", err)
 	}
 	}
-	err = os.WriteFile(csConfig.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
+	err = os.WriteFile(cfg.ConfigPaths.SimulationFilePath, newConfigSim, 0o644)
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("unable to write new simulation config in '%s' : %s", csConfig.ConfigPaths.SimulationFilePath, err)
+		return fmt.Errorf("unable to write new simulation config in '%s' : %s", cfg.ConfigPaths.SimulationFilePath, err)
 	}
 	}
 
 
 	log.Printf("global simulation: disabled")
 	log.Printf("global simulation: disabled")
 	return nil
 	return nil
 }
 }
 
 
-func simulationStatus() error {
-	if csConfig.Cscli.SimulationConfig == nil {
+func (cli *cliSimulation) status() {
+	cfg := cli.cfg()
+	if cfg.Cscli.SimulationConfig == nil {
 		log.Printf("global simulation: disabled (configuration file is missing)")
 		log.Printf("global simulation: disabled (configuration file is missing)")
-		return nil
+		return
 	}
 	}
-	if *csConfig.Cscli.SimulationConfig.Simulation {
+	if *cfg.Cscli.SimulationConfig.Simulation {
 		log.Println("global simulation: enabled")
 		log.Println("global simulation: enabled")
-		if len(csConfig.Cscli.SimulationConfig.Exclusions) > 0 {
+		if len(cfg.Cscli.SimulationConfig.Exclusions) > 0 {
 			log.Println("Scenarios not in simulation mode :")
 			log.Println("Scenarios not in simulation mode :")
-			for _, scenario := range csConfig.Cscli.SimulationConfig.Exclusions {
+			for _, scenario := range cfg.Cscli.SimulationConfig.Exclusions {
 				log.Printf("  - %s", scenario)
 				log.Printf("  - %s", scenario)
 			}
 			}
 		}
 		}
 	} else {
 	} else {
 		log.Println("global simulation: disabled")
 		log.Println("global simulation: disabled")
-		if len(csConfig.Cscli.SimulationConfig.Exclusions) > 0 {
+		if len(cfg.Cscli.SimulationConfig.Exclusions) > 0 {
 			log.Println("Scenarios in simulation mode :")
 			log.Println("Scenarios in simulation mode :")
-			for _, scenario := range csConfig.Cscli.SimulationConfig.Exclusions {
+			for _, scenario := range cfg.Cscli.SimulationConfig.Exclusions {
 				log.Printf("  - %s", scenario)
 				log.Printf("  - %s", scenario)
 			}
 			}
 		}
 		}
 	}
 	}
-	return nil
 }
 }

+ 6 - 4
cmd/crowdsec-cli/support.go

@@ -149,19 +149,21 @@ func collectHubItems(hub *cwhub.Hub, itemType string) []byte {
 
 
 func collectBouncers(dbClient *database.Client) ([]byte, error) {
 func collectBouncers(dbClient *database.Client) ([]byte, error) {
 	out := bytes.NewBuffer(nil)
 	out := bytes.NewBuffer(nil)
-	err := getBouncers(out, dbClient)
+	bouncers, err := dbClient.ListBouncers()
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to list bouncers: %s", err)
 	}
 	}
+	getBouncersTable(out, bouncers)
 	return out.Bytes(), nil
 	return out.Bytes(), nil
 }
 }
 
 
 func collectAgents(dbClient *database.Client) ([]byte, error) {
 func collectAgents(dbClient *database.Client) ([]byte, error) {
 	out := bytes.NewBuffer(nil)
 	out := bytes.NewBuffer(nil)
-	err := getAgents(out, dbClient)
+	machines, err := dbClient.ListMachines()
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to list machines: %s", err)
 	}
 	}
+	getAgentsTable(out, machines)
 	return out.Bytes(), nil
 	return out.Bytes(), nil
 }
 }
 
 

+ 0 - 12
cmd/crowdsec-cli/utils.go

@@ -8,7 +8,6 @@ import (
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 
 
-	"github.com/crowdsecurity/crowdsec/pkg/database"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 )
 )
 
 
@@ -47,17 +46,6 @@ func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *
 	return nil
 	return nil
 }
 }
 
 
-func getDBClient() (*database.Client, error) {
-	if err := csConfig.LoadAPIServer(true); err != nil || csConfig.DisableAPI {
-		return nil, err
-	}
-	ret, err := database.NewClient(csConfig.DbConfig)
-	if err != nil {
-		return nil, err
-	}
-	return ret, nil
-}
-
 func removeFromSlice(val string, slice []string) []string {
 func removeFromSlice(val string, slice []string) []string {
 	var i int
 	var i int
 	var value string
 	var value string

+ 1 - 0
docker/README.md

@@ -316,6 +316,7 @@ config.yaml) each time the container is run.
 | `BOUNCERS_ALLOWED_OU`   | bouncer-ou | OU values allowed for bouncers, separated by comma |
 | `BOUNCERS_ALLOWED_OU`   | bouncer-ou | OU values allowed for bouncers, separated by comma |
 |                         | | |
 |                         | | |
 | __Hub management__      | | |
 | __Hub management__      | | |
+| `NO_HUB_UPGRADE`        | false | Skip hub update / upgrade when the container starts |
 | `COLLECTIONS`           | | Collections to install, separated by space: `-e COLLECTIONS="crowdsecurity/linux crowdsecurity/apache2"` |
 | `COLLECTIONS`           | | Collections to install, separated by space: `-e COLLECTIONS="crowdsecurity/linux crowdsecurity/apache2"` |
 | `PARSERS`               | | Parsers to install, separated by space |
 | `PARSERS`               | | Parsers to install, separated by space |
 | `SCENARIOS`             | | Scenarios to install, separated by space |
 | `SCENARIOS`             | | Scenarios to install, separated by space |

+ 5 - 1
docker/docker_start.sh

@@ -303,8 +303,12 @@ fi
 conf_set_if "$PLUGIN_DIR" '.config_paths.plugin_dir = strenv(PLUGIN_DIR)'
 conf_set_if "$PLUGIN_DIR" '.config_paths.plugin_dir = strenv(PLUGIN_DIR)'
 
 
 ## Install hub items
 ## Install hub items
+
 cscli hub update || true
 cscli hub update || true
-cscli hub upgrade || true
+
+if isfalse "$NO_HUB_UPGRADE"; then
+    cscli hub upgrade || true
+fi
 
 
 cscli_if_clean parsers install crowdsecurity/docker-logs
 cscli_if_clean parsers install crowdsecurity/docker-logs
 cscli_if_clean parsers install crowdsecurity/cri-logs
 cscli_if_clean parsers install crowdsecurity/cri-logs

+ 1 - 1
docker/test/Pipfile

@@ -1,7 +1,7 @@
 [packages]
 [packages]
 pytest-dotenv = "0.5.2"
 pytest-dotenv = "0.5.2"
 pytest-xdist = "3.5.0"
 pytest-xdist = "3.5.0"
-pytest-cs = {ref = "0.7.18", git = "https://github.com/crowdsecurity/pytest-cs.git"}
+pytest-cs = {ref = "0.7.19", git = "https://github.com/crowdsecurity/pytest-cs.git"}
 
 
 [dev-packages]
 [dev-packages]
 gnureadline = "8.1.2"
 gnureadline = "8.1.2"

+ 76 - 66
docker/test/Pipfile.lock

@@ -1,7 +1,7 @@
 {
 {
     "_meta": {
     "_meta": {
         "hash": {
         "hash": {
-            "sha256": "575cb97d0b7fb66caf843191b843724307f7bc39c3c160f22330ba38ee055c80"
+            "sha256": "b5d25a7199d15a900b285be1af97cf7b7083c6637d631ad777b454471c8319fe"
         },
         },
         "pipfile-spec": 6,
         "pipfile-spec": 6,
         "requires": {
         "requires": {
@@ -79,7 +79,7 @@
                 "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
                 "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956",
                 "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
                 "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"
             ],
             ],
-            "markers": "python_version >= '3.8'",
+            "markers": "platform_python_implementation != 'PyPy'",
             "version": "==1.16.0"
             "version": "==1.16.0"
         },
         },
         "charset-normalizer": {
         "charset-normalizer": {
@@ -180,32 +180,41 @@
         },
         },
         "cryptography": {
         "cryptography": {
             "hashes": [
             "hashes": [
-                "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960",
-                "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a",
-                "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc",
-                "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a",
-                "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf",
-                "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1",
-                "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39",
-                "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406",
-                "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a",
-                "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a",
-                "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c",
-                "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be",
-                "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15",
-                "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2",
-                "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d",
-                "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157",
-                "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003",
-                "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248",
-                "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a",
-                "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec",
-                "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309",
-                "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7",
-                "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"
+                "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380",
+                "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589",
+                "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea",
+                "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65",
+                "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a",
+                "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3",
+                "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008",
+                "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1",
+                "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2",
+                "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635",
+                "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2",
+                "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90",
+                "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee",
+                "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a",
+                "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242",
+                "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12",
+                "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2",
+                "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d",
+                "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be",
+                "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee",
+                "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6",
+                "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529",
+                "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929",
+                "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1",
+                "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6",
+                "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a",
+                "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446",
+                "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9",
+                "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888",
+                "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4",
+                "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33",
+                "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"
             ],
             ],
             "markers": "python_version >= '3.7'",
             "markers": "python_version >= '3.7'",
-            "version": "==41.0.7"
+            "version": "==42.0.2"
         },
         },
         "docker": {
         "docker": {
             "hashes": [
             "hashes": [
@@ -249,33 +258,33 @@
         },
         },
         "pluggy": {
         "pluggy": {
             "hashes": [
             "hashes": [
-                "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
-                "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
+                "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+                "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
             ],
             ],
             "markers": "python_version >= '3.8'",
             "markers": "python_version >= '3.8'",
-            "version": "==1.3.0"
+            "version": "==1.4.0"
         },
         },
         "psutil": {
         "psutil": {
             "hashes": [
             "hashes": [
-                "sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340",
-                "sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6",
-                "sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284",
-                "sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c",
-                "sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7",
-                "sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c",
-                "sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e",
-                "sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6",
-                "sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056",
-                "sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9",
-                "sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68",
-                "sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df",
-                "sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e",
-                "sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414",
-                "sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508",
-                "sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe"
+                "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d",
+                "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73",
+                "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8",
+                "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2",
+                "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e",
+                "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36",
+                "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7",
+                "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c",
+                "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee",
+                "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421",
+                "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf",
+                "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81",
+                "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0",
+                "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631",
+                "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4",
+                "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"
             ],
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==5.9.7"
+            "version": "==5.9.8"
         },
         },
         "pycparser": {
         "pycparser": {
             "hashes": [
             "hashes": [
@@ -286,15 +295,15 @@
         },
         },
         "pytest": {
         "pytest": {
             "hashes": [
             "hashes": [
-                "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac",
-                "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"
+                "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c",
+                "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"
             ],
             ],
-            "markers": "python_version >= '3.7'",
-            "version": "==7.4.3"
+            "markers": "python_version >= '3.8'",
+            "version": "==8.0.0"
         },
         },
         "pytest-cs": {
         "pytest-cs": {
             "git": "https://github.com/crowdsecurity/pytest-cs.git",
             "git": "https://github.com/crowdsecurity/pytest-cs.git",
-            "ref": "df835beabc539be7f7f627b21caa0d6ad333daae"
+            "ref": "aea7e8549faa32f5e1d1f17755a5db3712396a2a"
         },
         },
         "pytest-datadir": {
         "pytest-datadir": {
             "hashes": [
             "hashes": [
@@ -322,11 +331,11 @@
         },
         },
         "python-dotenv": {
         "python-dotenv": {
             "hashes": [
             "hashes": [
-                "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba",
-                "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"
+                "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
+                "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
             ],
             ],
             "markers": "python_version >= '3.8'",
             "markers": "python_version >= '3.8'",
-            "version": "==1.0.0"
+            "version": "==1.0.1"
         },
         },
         "pyyaml": {
         "pyyaml": {
             "hashes": [
             "hashes": [
@@ -359,6 +368,7 @@
                 "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
                 "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
                 "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
                 "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
                 "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
                 "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
                 "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
                 "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
                 "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
                 "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
                 "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
                 "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
@@ -402,11 +412,11 @@
         },
         },
         "urllib3": {
         "urllib3": {
             "hashes": [
             "hashes": [
-                "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3",
-                "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"
+                "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20",
+                "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"
             ],
             ],
             "markers": "python_version >= '3.8'",
             "markers": "python_version >= '3.8'",
-            "version": "==2.1.0"
+            "version": "==2.2.0"
         }
         }
     },
     },
     "develop": {
     "develop": {
@@ -476,11 +486,11 @@
         },
         },
         "ipython": {
         "ipython": {
             "hashes": [
             "hashes": [
-                "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27",
-                "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"
+                "sha256:1050a3ab8473488d7eee163796b02e511d0735cf43a04ba2a8348bd0f2eaf8a5",
+                "sha256:48fbc236fbe0e138b88773fa0437751f14c3645fb483f1d4c5dee58b37e5ce73"
             ],
             ],
             "markers": "python_version >= '3.11'",
             "markers": "python_version >= '3.11'",
-            "version": "==8.18.1"
+            "version": "==8.21.0"
         },
         },
         "jedi": {
         "jedi": {
             "hashes": [
             "hashes": [
@@ -561,18 +571,18 @@
         },
         },
         "traitlets": {
         "traitlets": {
             "hashes": [
             "hashes": [
-                "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33",
-                "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"
+                "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74",
+                "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"
             ],
             ],
             "markers": "python_version >= '3.8'",
             "markers": "python_version >= '3.8'",
-            "version": "==5.14.0"
+            "version": "==5.14.1"
         },
         },
         "wcwidth": {
         "wcwidth": {
             "hashes": [
             "hashes": [
-                "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02",
-                "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"
+                "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859",
+                "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"
             ],
             ],
-            "version": "==0.2.12"
+            "version": "==0.2.13"
         }
         }
     }
     }
 }
 }

+ 5 - 5
docker/test/tests/test_tls.py

@@ -241,7 +241,7 @@ def test_tls_mutual_split_lapi_agent(crowdsec, flavor, certs_dir):
             assert "You can successfully interact with Local API (LAPI)" in stdout
             assert "You can successfully interact with Local API (LAPI)" in stdout
 
 
 
 
-def test_tls_client_ou(crowdsec, certs_dir):
+def test_tls_client_ou(crowdsec, flavor, certs_dir):
     """Check behavior of client certificate vs AGENTS_ALLOWED_OU"""
     """Check behavior of client certificate vs AGENTS_ALLOWED_OU"""
 
 
     rand = uuid.uuid1()
     rand = uuid.uuid1()
@@ -270,8 +270,8 @@ def test_tls_client_ou(crowdsec, certs_dir):
         certs_dir(lapi_hostname=lapiname, agent_ou='custom-client-ou'): {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
         certs_dir(lapi_hostname=lapiname, agent_ou='custom-client-ou'): {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
     }
     }
 
 
-    cs_lapi = crowdsec(name=lapiname, environment=lapi_env, volumes=volumes)
-    cs_agent = crowdsec(name=agentname, environment=agent_env, volumes=volumes)
+    cs_lapi = crowdsec(flavor=flavor, name=lapiname, environment=lapi_env, volumes=volumes)
+    cs_agent = crowdsec(flavor=flavor, name=agentname, environment=agent_env, volumes=volumes)
 
 
     with cs_lapi as lapi:
     with cs_lapi as lapi:
         lapi.wait_for_log([
         lapi.wait_for_log([
@@ -300,8 +300,8 @@ def test_tls_client_ou(crowdsec, certs_dir):
         certs_dir(lapi_hostname=lapiname, agent_ou='custom-client-ou'): {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
         certs_dir(lapi_hostname=lapiname, agent_ou='custom-client-ou'): {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
     }
     }
 
 
-    cs_lapi = crowdsec(name=lapiname, environment=lapi_env, volumes=volumes)
-    cs_agent = crowdsec(name=agentname, environment=agent_env, volumes=volumes)
+    cs_lapi = crowdsec(flavor=flavor, name=lapiname, environment=lapi_env, volumes=volumes)
+    cs_agent = crowdsec(flavor=flavor, name=agentname, environment=agent_env, volumes=volumes)
 
 
     with cs_lapi as lapi:
     with cs_lapi as lapi:
         lapi.wait_for_log([
         lapi.wait_for_log([

+ 2 - 2
pkg/csconfig/api.go

@@ -285,7 +285,7 @@ func (c *Config) LoadAPIServer(inCli bool) error {
 		}
 		}
 	}
 	}
 
 
-	if c.API.Server.OnlineClient == nil || c.API.Server.OnlineClient.Credentials == nil {
+	if (c.API.Server.OnlineClient == nil || c.API.Server.OnlineClient.Credentials == nil) && !inCli {
 		log.Printf("push and pull to Central API disabled")
 		log.Printf("push and pull to Central API disabled")
 	}
 	}
 
 
@@ -297,7 +297,7 @@ func (c *Config) LoadAPIServer(inCli bool) error {
 		return err
 		return err
 	}
 	}
 
 
-	if c.API.Server.CapiWhitelistsPath != "" {
+	if c.API.Server.CapiWhitelistsPath != "" && !inCli {
 		log.Infof("loaded capi whitelist from %s: %d IPs, %d CIDRs", c.API.Server.CapiWhitelistsPath, len(c.API.Server.CapiWhitelists.Ips), len(c.API.Server.CapiWhitelists.Cidrs))
 		log.Infof("loaded capi whitelist from %s: %d IPs, %d CIDRs", c.API.Server.CapiWhitelistsPath, len(c.API.Server.CapiWhitelists.Ips), len(c.API.Server.CapiWhitelists.Cidrs))
 	}
 	}
 
 

+ 2 - 2
pkg/csconfig/config.go

@@ -41,9 +41,9 @@ type Config struct {
 	Hub          *LocalHubCfg        `yaml:"-"`
 	Hub          *LocalHubCfg        `yaml:"-"`
 }
 }
 
 
-func NewConfig(configFile string, disableAgent bool, disableAPI bool, quiet bool) (*Config, string, error) {
+func NewConfig(configFile string, disableAgent bool, disableAPI bool, inCli bool) (*Config, string, error) {
 	patcher := yamlpatch.NewPatcher(configFile, ".local")
 	patcher := yamlpatch.NewPatcher(configFile, ".local")
-	patcher.SetQuiet(quiet)
+	patcher.SetQuiet(inCli)
 	fcontent, err := patcher.MergedPatchContent()
 	fcontent, err := patcher.MergedPatchContent()
 	if err != nil {
 	if err != nil {
 		return nil, "", err
 		return nil, "", err

+ 8 - 3
test/bats/30_machines.bats

@@ -34,13 +34,18 @@ teardown() {
     rune -0 jq -r '.msg' <(stderr)
     rune -0 jq -r '.msg' <(stderr)
     assert_output --partial 'already exists: please remove it, use "--force" or specify a different file with "-f"'
     assert_output --partial 'already exists: please remove it, use "--force" or specify a different file with "-f"'
     rune -0 cscli machines add local -a --force
     rune -0 cscli machines add local -a --force
-    assert_output --partial "Machine 'local' successfully added to the local API."
+    assert_stderr --partial "Machine 'local' successfully added to the local API."
+}
+
+@test "passwords have a size limit" {
+    rune -1 cscli machines add local --password "$(printf '%73s' '' | tr ' ' x)"
+    assert_stderr --partial "password too long (max 72 characters)"
 }
 }
 
 
 @test "add a new machine and delete it" {
 @test "add a new machine and delete it" {
     rune -0 cscli machines add -a -f /dev/null CiTestMachine -o human
     rune -0 cscli machines add -a -f /dev/null CiTestMachine -o human
-    assert_output --partial "Machine 'CiTestMachine' successfully added to the local API"
-    assert_output --partial "API credentials written to '/dev/null'"
+    assert_stderr --partial "Machine 'CiTestMachine' successfully added to the local API"
+    assert_stderr --partial "API credentials written to '/dev/null'"
 
 
     # we now have two machines
     # we now have two machines
     rune -0 cscli machines list -o json
     rune -0 cscli machines list -o json