From ca3e979aecd8b010e4588e316bdb3ea54bce98d9 Mon Sep 17 00:00:00 2001 From: marco Date: Mon, 21 Oct 2024 11:19:27 +0200 Subject: [PATCH 1/6] refact "cscli decisions import" --- .../{decisions_import.go => import.go} | 116 +++++++----------- .../{decisions_table.go => table.go} | 0 test/bats/90_decisions.bats | 8 +- 3 files changed, 50 insertions(+), 74 deletions(-) rename cmd/crowdsec-cli/clidecision/{decisions_import.go => import.go} (71%) rename cmd/crowdsec-cli/clidecision/{decisions_table.go => table.go} (100%) diff --git a/cmd/crowdsec-cli/clidecision/decisions_import.go b/cmd/crowdsec-cli/clidecision/import.go similarity index 71% rename from cmd/crowdsec-cli/clidecision/decisions_import.go rename to cmd/crowdsec-cli/clidecision/import.go index 10d92f88876..5b34b74a250 100644 --- a/cmd/crowdsec-cli/clidecision/decisions_import.go +++ b/cmd/crowdsec-cli/clidecision/import.go @@ -67,65 +67,29 @@ func parseDecisionList(content []byte, format string) ([]decisionRaw, error) { return ret, nil } -func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error { - flags := cmd.Flags() - - input, err := flags.GetString("input") - if err != nil { - return err - } - - defaultDuration, err := flags.GetString("duration") - if err != nil { - return err - } - - if defaultDuration == "" { - return errors.New("--duration cannot be empty") - } - - defaultScope, err := flags.GetString("scope") - if err != nil { - return err - } - - if defaultScope == "" { - return errors.New("--scope cannot be empty") - } - - defaultReason, err := flags.GetString("reason") - if err != nil { - return err - } - - if defaultReason == "" { - return errors.New("--reason cannot be empty") - } +func (cli *cliDecisions) import_(ctx context.Context, input string, duration string, scope string, reason string, type_ string, batch int, format string) error { + var ( + content []byte + fin *os.File + err error + ) - defaultType, err := flags.GetString("type") - if err != nil { - return err + if duration == "" { + return errors.New("default duration cannot be empty") } - if defaultType == "" { - return errors.New("--type cannot be empty") + if scope == "" { + return errors.New("default scope cannot be empty") } - batchSize, err := flags.GetInt("batch") - if err != nil { - return err + if reason == "" { + return errors.New("default reason cannot be empty") } - format, err := flags.GetString("format") - if err != nil { - return err + if type_ == "" { + return errors.New("default type cannot be empty") } - var ( - content []byte - fin *os.File - ) - // set format if the file has a json or csv extension if format == "" { if strings.HasSuffix(input, ".json") { @@ -167,23 +131,23 @@ func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error { } if d.Duration == "" { - d.Duration = defaultDuration - log.Debugf("item %d: missing 'duration', using default '%s'", i, defaultDuration) + d.Duration = duration + log.Debugf("item %d: missing 'duration', using default '%s'", i, duration) } if d.Scenario == "" { - d.Scenario = defaultReason - log.Debugf("item %d: missing 'reason', using default '%s'", i, defaultReason) + d.Scenario = reason + log.Debugf("item %d: missing 'reason', using default '%s'", i, reason) } if d.Type == "" { - d.Type = defaultType - log.Debugf("item %d: missing 'type', using default '%s'", i, defaultType) + d.Type = type_ + log.Debugf("item %d: missing 'type', using default '%s'", i, type_) } if d.Scope == "" { - d.Scope = defaultScope - log.Debugf("item %d: missing 'scope', using default '%s'", i, defaultScope) + d.Scope = scope + log.Debugf("item %d: missing 'scope', using default '%s'", i, scope) } decisions[i] = &models.Decision{ @@ -201,7 +165,7 @@ func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error { log.Infof("You are about to add %d decisions, this may take a while", len(decisions)) } - for _, chunk := range slicetools.Chunks(decisions, batchSize) { + for _, chunk := range slicetools.Chunks(decisions, batch) { log.Debugf("Processing chunk of %d decisions", len(chunk)) importAlert := models.Alert{ CreatedAt: time.Now().UTC().Format(time.RFC3339), @@ -224,7 +188,7 @@ func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error { Decisions: chunk, } - _, _, err = cli.client.Alerts.Add(context.Background(), models.AddAlertsRequest{&importAlert}) + _, _, err = cli.client.Alerts.Add(ctx, models.AddAlertsRequest{&importAlert}) if err != nil { return err } @@ -236,12 +200,22 @@ func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error { } func (cli *cliDecisions) newImportCmd() *cobra.Command { + var ( + input string + duration string + scope string + reason string + decisionType string + batch int + format string + ) + cmd := &cobra.Command{ Use: "import [options]", Short: "Import decisions from a file or pipe", Long: "expected format:\n" + "csv : any of duration,reason,scope,type,value, with a header line\n" + - "json :" + "`{" + `"duration" : "24h", "reason" : "my_scenario", "scope" : "ip", "type" : "ban", "value" : "x.y.z.z"` + "}`", + "json :" + "`{" + `"duration": "24h", "reason": "my_scenario", "scope": "ip", "type": "ban", "value": "x.y.z.z"` + "}`", Args: cobra.NoArgs, DisableAutoGenTag: true, Example: `decisions.csv: @@ -251,7 +225,7 @@ duration,scope,value $ cscli decisions import -i decisions.csv decisions.json: -[{"duration" : "4h", "scope" : "ip", "type" : "ban", "value" : "1.2.3.4"}] +[{"duration": "4h", "scope": "ip", "type": "ban", "value": "1.2.3.4"}] The file format is detected from the extension, but can be forced with the --format option which is required when reading from standard input. @@ -260,18 +234,20 @@ Raw values, standard input: $ echo "1.2.3.4" | cscli decisions import -i - --format values `, - RunE: cli.runImport, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.import_(cmd.Context(), input, duration, scope, reason, decisionType, batch, format) + }, } flags := cmd.Flags() flags.SortFlags = false - flags.StringP("input", "i", "", "Input file") - flags.StringP("duration", "d", "4h", "Decision duration: 1h,4h,30m") - flags.String("scope", types.Ip, "Decision scope: ip,range,username") - flags.StringP("reason", "R", "manual", "Decision reason: ") - flags.StringP("type", "t", "ban", "Decision type: ban,captcha,throttle") - flags.Int("batch", 0, "Split import in batches of N decisions") - flags.String("format", "", "Input format: 'json', 'csv' or 'values' (each line is a value, no headers)") + flags.StringVarP(&input, "input", "i", "", "Input file") + flags.StringVarP(&duration, "duration", "d", "4h", "Decision duration: 1h,4h,30m") + flags.StringVar(&scope, "scope", types.Ip, "Decision scope: ip,range,username") + flags.StringVarP(&reason, "reason", "R", "manual", "Decision reason: ") + flags.StringVarP(&decisionType, "type", "t", "ban", "Decision type: ban,captcha,throttle") + flags.IntVar(&batch, "batch", 0, "Split import in batches of N decisions") + flags.StringVar(&format, "format", "", "Input format: 'json', 'csv' or 'values' (each line is a value, no headers)") _ = cmd.MarkFlagRequired("input") diff --git a/cmd/crowdsec-cli/clidecision/decisions_table.go b/cmd/crowdsec-cli/clidecision/table.go similarity index 100% rename from cmd/crowdsec-cli/clidecision/decisions_table.go rename to cmd/crowdsec-cli/clidecision/table.go diff --git a/test/bats/90_decisions.bats b/test/bats/90_decisions.bats index b892dc84015..8601414db48 100644 --- a/test/bats/90_decisions.bats +++ b/test/bats/90_decisions.bats @@ -78,13 +78,13 @@ teardown() { # invalid defaults rune -1 cscli decisions import --duration "" -i - <<<'value\n5.6.7.8' --format csv - assert_stderr --partial "--duration cannot be empty" + assert_stderr --partial "default duration cannot be empty" rune -1 cscli decisions import --scope "" -i - <<<'value\n5.6.7.8' --format csv - assert_stderr --partial "--scope cannot be empty" + assert_stderr --partial "default scope cannot be empty" rune -1 cscli decisions import --reason "" -i - <<<'value\n5.6.7.8' --format csv - assert_stderr --partial "--reason cannot be empty" + assert_stderr --partial "default reason cannot be empty" rune -1 cscli decisions import --type "" -i - <<<'value\n5.6.7.8' --format csv - assert_stderr --partial "--type cannot be empty" + assert_stderr --partial "default type cannot be empty" #---------- # JSON From 536380e48722a27da966ae5ad2829ae82a01f09d Mon Sep 17 00:00:00 2001 From: marco Date: Mon, 23 Sep 2024 21:50:20 +0200 Subject: [PATCH 2/6] cobra.ExactArgs(0) -> cobra.NoArgs --- cmd/crowdsec-cli/clialert/alerts.go | 2 +- cmd/crowdsec-cli/clibouncer/bouncers.go | 2 +- cmd/crowdsec-cli/clidecision/decisions.go | 4 ++-- cmd/crowdsec-cli/cliexplain/explain.go | 2 +- cmd/crowdsec-cli/clihub/hub.go | 10 +++++----- cmd/crowdsec-cli/clihubtest/hubtest.go | 2 +- cmd/crowdsec-cli/climetrics/list.go | 2 +- cmd/crowdsec-cli/climetrics/metrics.go | 2 +- cmd/crowdsec-cli/clinotifications/notifications.go | 2 +- cmd/crowdsec-cli/config.go | 2 +- cmd/crowdsec-cli/config_feature_flags.go | 2 +- cmd/crowdsec-cli/config_show.go | 2 +- cmd/crowdsec-cli/config_showyaml.go | 2 +- cmd/crowdsec-cli/dashboard.go | 10 +++++----- 14 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/crowdsec-cli/clialert/alerts.go b/cmd/crowdsec-cli/clialert/alerts.go index 75454e945f2..425b9860fc9 100644 --- a/cmd/crowdsec-cli/clialert/alerts.go +++ b/cmd/crowdsec-cli/clialert/alerts.go @@ -465,7 +465,7 @@ cscli alerts delete --range 1.2.3.0/24 cscli alerts delete -s crowdsecurity/ssh-bf"`, DisableAutoGenTag: true, Aliases: []string{"remove"}, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, _ []string) error { if deleteAll { return nil diff --git a/cmd/crowdsec-cli/clibouncer/bouncers.go b/cmd/crowdsec-cli/clibouncer/bouncers.go index 226fbb7e922..960f6a60815 100644 --- a/cmd/crowdsec-cli/clibouncer/bouncers.go +++ b/cmd/crowdsec-cli/clibouncer/bouncers.go @@ -198,7 +198,7 @@ func (cli *cliBouncers) newListCmd() *cobra.Command { Use: "list", Short: "list all bouncers within the database", Example: `cscli bouncers list`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.List(cmd.Context(), color.Output, cli.db) diff --git a/cmd/crowdsec-cli/clidecision/decisions.go b/cmd/crowdsec-cli/clidecision/decisions.go index 1f8781a3716..307cabffe51 100644 --- a/cmd/crowdsec-cli/clidecision/decisions.go +++ b/cmd/crowdsec-cli/clidecision/decisions.go @@ -290,7 +290,7 @@ cscli decisions list -r 1.2.3.0/24 cscli decisions list -s crowdsecurity/ssh-bf cscli decisions list --origin lists --scenario list_name `, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.list(cmd.Context(), filter, NoSimu, contained, printMachine) @@ -416,7 +416,7 @@ cscli decisions add --ip 1.2.3.4 --duration 24h --type captcha cscli decisions add --scope username --value foobar `, /*TBD : fix long and example*/ - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType) diff --git a/cmd/crowdsec-cli/cliexplain/explain.go b/cmd/crowdsec-cli/cliexplain/explain.go index 182e34a12a5..c7337a86024 100644 --- a/cmd/crowdsec-cli/cliexplain/explain.go +++ b/cmd/crowdsec-cli/cliexplain/explain.go @@ -80,7 +80,7 @@ cscli explain --log "Sep 19 18:33:22 scw-d95986 sshd[24347]: pam_unix(sshd:auth) cscli explain --dsn "file://myfile.log" --type nginx tail -n 5 myfile.log | cscli explain --type nginx -f - `, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { return cli.run() diff --git a/cmd/crowdsec-cli/clihub/hub.go b/cmd/crowdsec-cli/clihub/hub.go index 22568355546..f189d6a2e13 100644 --- a/cmd/crowdsec-cli/clihub/hub.go +++ b/cmd/crowdsec-cli/clihub/hub.go @@ -39,7 +39,7 @@ The Hub is managed by cscli, to get the latest hub files from [Crowdsec Hub](htt Example: `cscli hub list cscli hub update cscli hub upgrade`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, } @@ -87,7 +87,7 @@ func (cli *cliHub) newListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list [-a]", Short: "List all installed configurations", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { hub, err := require.Hub(cli.cfg(), nil, log.StandardLogger()) @@ -140,7 +140,7 @@ func (cli *cliHub) newUpdateCmd() *cobra.Command { Long: ` Fetches the .index.json file from the hub, containing the list of available configs. `, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.update(cmd.Context(), withContent) @@ -190,7 +190,7 @@ func (cli *cliHub) newUpgradeCmd() *cobra.Command { Long: ` Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if you want the latest versions available. `, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.upgrade(cmd.Context(), force) @@ -235,7 +235,7 @@ func (cli *cliHub) newTypesCmd() *cobra.Command { Long: ` List the types of supported hub items. `, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { return cli.types() diff --git a/cmd/crowdsec-cli/clihubtest/hubtest.go b/cmd/crowdsec-cli/clihubtest/hubtest.go index 3420e21e1e2..f4cfed2e1cb 100644 --- a/cmd/crowdsec-cli/clihubtest/hubtest.go +++ b/cmd/crowdsec-cli/clihubtest/hubtest.go @@ -39,7 +39,7 @@ func (cli *cliHubTest) NewCommand() *cobra.Command { Use: "hubtest", Short: "Run functional tests on hub configurations", Long: "Run functional tests on hub configurations (parsers, scenarios, collections...)", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, PersistentPreRunE: func(_ *cobra.Command, _ []string) error { var err error diff --git a/cmd/crowdsec-cli/climetrics/list.go b/cmd/crowdsec-cli/climetrics/list.go index ddb2baac14d..27fa99710c8 100644 --- a/cmd/crowdsec-cli/climetrics/list.go +++ b/cmd/crowdsec-cli/climetrics/list.go @@ -84,7 +84,7 @@ func (cli *cliMetrics) newListCmd() *cobra.Command { Use: "list", Short: "List available types of metrics.", Long: `List available types of metrics.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { return cli.list() diff --git a/cmd/crowdsec-cli/climetrics/metrics.go b/cmd/crowdsec-cli/climetrics/metrics.go index f3bc4874460..67bd7b6ad93 100644 --- a/cmd/crowdsec-cli/climetrics/metrics.go +++ b/cmd/crowdsec-cli/climetrics/metrics.go @@ -36,7 +36,7 @@ cscli metrics --url http://lapi.local:6060/metrics show acquisition parsers # List available metric types cscli metrics list`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, _ []string) error { return cli.show(cmd.Context(), nil, url, noUnit) diff --git a/cmd/crowdsec-cli/clinotifications/notifications.go b/cmd/crowdsec-cli/clinotifications/notifications.go index 5489faa37c8..baf899c10cf 100644 --- a/cmd/crowdsec-cli/clinotifications/notifications.go +++ b/cmd/crowdsec-cli/clinotifications/notifications.go @@ -158,7 +158,7 @@ func (cli *cliNotifications) newListCmd() *cobra.Command { Short: "list notifications plugins", Long: `list notifications plugins and their status (active or not)`, Example: `cscli notifications list`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { cfg := cli.cfg() diff --git a/cmd/crowdsec-cli/config.go b/cmd/crowdsec-cli/config.go index e88845798e2..4cf8916ad4b 100644 --- a/cmd/crowdsec-cli/config.go +++ b/cmd/crowdsec-cli/config.go @@ -18,7 +18,7 @@ func (cli *cliConfig) NewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "config [command]", Short: "Allows to view current config", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, } diff --git a/cmd/crowdsec-cli/config_feature_flags.go b/cmd/crowdsec-cli/config_feature_flags.go index d1dbe2b93b7..760e2194bb3 100644 --- a/cmd/crowdsec-cli/config_feature_flags.go +++ b/cmd/crowdsec-cli/config_feature_flags.go @@ -121,7 +121,7 @@ func (cli *cliConfig) newFeatureFlagsCmd() *cobra.Command { Use: "feature-flags", Short: "Displays feature flag status", Long: `Displays the supported feature flags and their current status.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { return cli.featureFlags(showRetired) diff --git a/cmd/crowdsec-cli/config_show.go b/cmd/crowdsec-cli/config_show.go index 2d3ac488ba2..3d17d264574 100644 --- a/cmd/crowdsec-cli/config_show.go +++ b/cmd/crowdsec-cli/config_show.go @@ -235,7 +235,7 @@ func (cli *cliConfig) newShowCmd() *cobra.Command { Use: "show", Short: "Displays current config", Long: `Displays the current cli configuration.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { if err := cli.cfg().LoadAPIClient(); err != nil { diff --git a/cmd/crowdsec-cli/config_showyaml.go b/cmd/crowdsec-cli/config_showyaml.go index 52daee6a65e..10549648d09 100644 --- a/cmd/crowdsec-cli/config_showyaml.go +++ b/cmd/crowdsec-cli/config_showyaml.go @@ -15,7 +15,7 @@ func (cli *cliConfig) newShowYAMLCmd() *cobra.Command { cmd := &cobra.Command{ Use: "show-yaml", Short: "Displays merged config.yaml + config.yaml.local", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { return cli.showYAML() diff --git a/cmd/crowdsec-cli/dashboard.go b/cmd/crowdsec-cli/dashboard.go index 41db9e6cbf2..53a7dff85a0 100644 --- a/cmd/crowdsec-cli/dashboard.go +++ b/cmd/crowdsec-cli/dashboard.go @@ -129,7 +129,7 @@ func (cli *cliDashboard) newSetupCmd() *cobra.Command { Use: "setup", Short: "Setup a metabase container.", Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, Example: ` cscli dashboard setup @@ -198,7 +198,7 @@ func (cli *cliDashboard) newStartCmd() *cobra.Command { Use: "start", Short: "Start the metabase container.", Long: `Stats the metabase container using docker.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID) @@ -229,7 +229,7 @@ func (cli *cliDashboard) newStopCmd() *cobra.Command { Use: "stop", Short: "Stops the metabase container.", Long: `Stops the metabase container using docker.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { if err := metabase.StopContainer(metabaseContainerID); err != nil { @@ -245,7 +245,7 @@ func (cli *cliDashboard) newStopCmd() *cobra.Command { func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command { cmd := &cobra.Command{Use: "show-password", Short: "displays password of metabase.", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: func(_ *cobra.Command, _ []string) error { m := metabase.Metabase{} @@ -268,7 +268,7 @@ func (cli *cliDashboard) newRemoveCmd() *cobra.Command { Use: "remove", Short: "removes the metabase container.", Long: `removes the metabase container using docker.`, - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, DisableAutoGenTag: true, Example: ` cscli dashboard remove From 648915c8d7f8afdd5804169ac7d3597d5d41e9f7 Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 29 Oct 2024 23:38:10 +0100 Subject: [PATCH 3/6] refact cscli bouncers --- cmd/crowdsec-cli/clibouncer/add.go | 72 +++++ cmd/crowdsec-cli/clibouncer/bouncers.go | 364 ------------------------ cmd/crowdsec-cli/clibouncer/delete.go | 51 ++++ cmd/crowdsec-cli/clibouncer/inspect.go | 98 +++++++ cmd/crowdsec-cli/clibouncer/list.go | 117 ++++++++ cmd/crowdsec-cli/clibouncer/prune.go | 85 ++++++ 6 files changed, 423 insertions(+), 364 deletions(-) create mode 100644 cmd/crowdsec-cli/clibouncer/add.go create mode 100644 cmd/crowdsec-cli/clibouncer/delete.go create mode 100644 cmd/crowdsec-cli/clibouncer/inspect.go create mode 100644 cmd/crowdsec-cli/clibouncer/list.go create mode 100644 cmd/crowdsec-cli/clibouncer/prune.go diff --git a/cmd/crowdsec-cli/clibouncer/add.go b/cmd/crowdsec-cli/clibouncer/add.go new file mode 100644 index 00000000000..8c40507a996 --- /dev/null +++ b/cmd/crowdsec-cli/clibouncer/add.go @@ -0,0 +1,72 @@ +package clibouncer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/spf13/cobra" + + middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" + "github.com/crowdsecurity/crowdsec/pkg/types" +) + +func (cli *cliBouncers) add(ctx context.Context, bouncerName string, key string) error { + var err error + + keyLength := 32 + + if key == "" { + key, err = middlewares.GenerateAPIKey(keyLength) + if err != nil { + return fmt.Errorf("unable to generate api key: %w", err) + } + } + + _, err = cli.db.CreateBouncer(ctx, bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType) + if err != nil { + return fmt.Errorf("unable to create bouncer: %w", err) + } + + switch cli.cfg().Cscli.Output { + case "human": + 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") + case "raw": + fmt.Print(key) + case "json": + j, err := json.Marshal(key) + if err != nil { + return errors.New("unable to serialize api key") + } + + fmt.Print(string(j)) + } + + return nil +} + +func (cli *cliBouncers) newAddCmd() *cobra.Command { + var key string + + cmd := &cobra.Command{ + Use: "add MyBouncerName", + Short: "add a single bouncer to the database", + Example: `cscli bouncers add MyBouncerName +cscli bouncers add MyBouncerName --key `, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.add(cmd.Context(), args[0], key) + }, + } + + flags := cmd.Flags() + flags.StringP("length", "l", "", "length of the api key") + _ = flags.MarkDeprecated("length", "use --key instead") + flags.StringVarP(&key, "key", "k", "", "api key for the bouncer") + + return cmd +} diff --git a/cmd/crowdsec-cli/clibouncer/bouncers.go b/cmd/crowdsec-cli/clibouncer/bouncers.go index 960f6a60815..876b613be53 100644 --- a/cmd/crowdsec-cli/clibouncer/bouncers.go +++ b/cmd/crowdsec-cli/clibouncer/bouncers.go @@ -1,33 +1,17 @@ package clibouncer import ( - "context" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - "os" "slices" "strings" "time" - "github.com/fatih/color" - "github.com/jedib0t/go-pretty/v6/table" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" - middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/database/ent" - "github.com/crowdsecurity/crowdsec/pkg/database/ent/bouncer" - "github.com/crowdsecurity/crowdsec/pkg/emoji" - "github.com/crowdsecurity/crowdsec/pkg/types" ) type configGetter = func() *csconfig.Config @@ -80,27 +64,6 @@ Note: This command requires database direct access, so is intended to be run on return cmd } -func (cli *cliBouncers) listHuman(out io.Writer, bouncers ent.Bouncers) { - t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer - t.AppendHeader(table.Row{"Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type"}) - - for _, b := range bouncers { - revoked := emoji.CheckMark - if b.Revoked { - revoked = emoji.Prohibited - } - - lastPull := "" - if b.LastPull != nil { - lastPull = b.LastPull.Format(time.RFC3339) - } - - t.AppendRow(table.Row{b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType}) - } - - io.WriteString(out, t.Render()+"\n") -} - // bouncerInfo contains only the data we want for inspect/list type bouncerInfo struct { CreatedAt time.Time `json:"created_at"` @@ -132,141 +95,6 @@ func newBouncerInfo(b *ent.Bouncer) bouncerInfo { } } -func (cli *cliBouncers) listCSV(out io.Writer, bouncers ent.Bouncers) error { - 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" - } - - lastPull := "" - if b.LastPull != nil { - lastPull = b.LastPull.Format(time.RFC3339) - } - - if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil { - return fmt.Errorf("failed to write raw: %w", err) - } - } - - csvwriter.Flush() - - return nil -} - -func (cli *cliBouncers) List(ctx context.Context, out io.Writer, db *database.Client) error { - // XXX: must use the provided db object, the one in the struct might be nil - // (calling List directly skips the PersistentPreRunE) - - bouncers, err := db.ListBouncers(ctx) - if err != nil { - return fmt.Errorf("unable to list bouncers: %w", err) - } - - switch cli.cfg().Cscli.Output { - case "human": - cli.listHuman(out, bouncers) - case "json": - info := make([]bouncerInfo, 0, len(bouncers)) - for _, b := range bouncers { - info = append(info, newBouncerInfo(b)) - } - - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - - if err := enc.Encode(info); err != nil { - return errors.New("failed to serialize") - } - - return nil - case "raw": - return cli.listCSV(out, bouncers) - } - - return nil -} - -func (cli *cliBouncers) newListCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Short: "list all bouncers within the database", - Example: `cscli bouncers list`, - Args: cobra.NoArgs, - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return cli.List(cmd.Context(), color.Output, cli.db) - }, - } - - return cmd -} - -func (cli *cliBouncers) add(ctx context.Context, bouncerName string, key string) error { - var err error - - keyLength := 32 - - if key == "" { - key, err = middlewares.GenerateAPIKey(keyLength) - if err != nil { - return fmt.Errorf("unable to generate api key: %w", err) - } - } - - _, err = cli.db.CreateBouncer(ctx, bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType) - if err != nil { - return fmt.Errorf("unable to create bouncer: %w", err) - } - - switch cli.cfg().Cscli.Output { - case "human": - 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") - case "raw": - fmt.Print(key) - case "json": - j, err := json.Marshal(key) - if err != nil { - return errors.New("unable to serialize api key") - } - - fmt.Print(string(j)) - } - - return nil -} - -func (cli *cliBouncers) newAddCmd() *cobra.Command { - var key string - - cmd := &cobra.Command{ - Use: "add MyBouncerName", - Short: "add a single bouncer to the database", - Example: `cscli bouncers add MyBouncerName -cscli bouncers add MyBouncerName --key `, - Args: cobra.ExactArgs(1), - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, args []string) error { - return cli.add(cmd.Context(), args[0], key) - }, - } - - flags := cmd.Flags() - flags.StringP("length", "l", "", "length of the api key") - _ = flags.MarkDeprecated("length", "use --key instead") - flags.StringVarP(&key, "key", "k", "", "api key for the bouncer") - - return cmd -} - // validBouncerID returns a list of bouncer IDs for command completion func (cli *cliBouncers) validBouncerID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var err error @@ -303,195 +131,3 @@ func (cli *cliBouncers) validBouncerID(cmd *cobra.Command, args []string, toComp return ret, cobra.ShellCompDirectiveNoFileComp } - -func (cli *cliBouncers) delete(ctx context.Context, bouncers []string, ignoreMissing bool) error { - for _, bouncerID := range bouncers { - if err := cli.db.DeleteBouncer(ctx, bouncerID); err != nil { - var notFoundErr *database.BouncerNotFoundError - if ignoreMissing && errors.As(err, ¬FoundErr) { - return nil - } - - return fmt.Errorf("unable to delete bouncer: %w", err) - } - - log.Infof("bouncer '%s' deleted successfully", bouncerID) - } - - return nil -} - -func (cli *cliBouncers) newDeleteCmd() *cobra.Command { - var ignoreMissing bool - - cmd := &cobra.Command{ - Use: "delete MyBouncerName", - Short: "delete bouncer(s) from the database", - Example: `cscli bouncers delete "bouncer1" "bouncer2"`, - Args: cobra.MinimumNArgs(1), - Aliases: []string{"remove"}, - DisableAutoGenTag: true, - ValidArgsFunction: cli.validBouncerID, - RunE: func(cmd *cobra.Command, args []string) error { - return cli.delete(cmd.Context(), args, ignoreMissing) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&ignoreMissing, "ignore-missing", false, "don't print errors if one or more bouncers don't exist") - - return cmd -} - -func (cli *cliBouncers) prune(ctx context.Context, duration time.Duration, force bool) error { - if duration < 2*time.Minute { - if yes, err := ask.YesNo( - "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.QueryBouncersInactiveSince(ctx, 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 - } - - cli.listHuman(color.Output, bouncers) - - if !force { - if yes, err := ask.YesNo( - "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(ctx, bouncers) - if err != nil { - return fmt.Errorf("unable to prune bouncers: %w", 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{ - Use: "prune", - Short: "prune multiple bouncers from the database", - Args: cobra.NoArgs, - DisableAutoGenTag: true, - Example: `cscli bouncers prune -d 45m -cscli bouncers prune -d 45m --force`, - RunE: func(cmd *cobra.Command, _ []string) error { - return cli.prune(cmd.Context(), duration, force) - }, - } - - 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 -} - -func (cli *cliBouncers) inspectHuman(out io.Writer, bouncer *ent.Bouncer) { - t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer - - t.SetTitle("Bouncer: " + bouncer.Name) - - t.SetColumnConfigs([]table.ColumnConfig{ - {Number: 1, AutoMerge: true}, - }) - - lastPull := "" - if bouncer.LastPull != nil { - lastPull = bouncer.LastPull.String() - } - - t.AppendRows([]table.Row{ - {"Created At", bouncer.CreatedAt}, - {"Last Update", bouncer.UpdatedAt}, - {"Revoked?", bouncer.Revoked}, - {"IP Address", bouncer.IPAddress}, - {"Type", bouncer.Type}, - {"Version", bouncer.Version}, - {"Last Pull", lastPull}, - {"Auth type", bouncer.AuthType}, - {"OS", clientinfo.GetOSNameAndVersion(bouncer)}, - }) - - for _, ff := range clientinfo.GetFeatureFlagList(bouncer) { - t.AppendRow(table.Row{"Feature Flags", ff}) - } - - io.WriteString(out, t.Render()+"\n") -} - -func (cli *cliBouncers) inspect(bouncer *ent.Bouncer) error { - out := color.Output - outputFormat := cli.cfg().Cscli.Output - - switch outputFormat { - case "human": - cli.inspectHuman(out, bouncer) - case "json": - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - - if err := enc.Encode(newBouncerInfo(bouncer)); err != nil { - return errors.New("failed to serialize") - } - - return nil - default: - return fmt.Errorf("output format '%s' not supported for this command", outputFormat) - } - - return nil -} - -func (cli *cliBouncers) newInspectCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "inspect [bouncer_name]", - Short: "inspect a bouncer by name", - Example: `cscli bouncers inspect "bouncer1"`, - Args: cobra.ExactArgs(1), - DisableAutoGenTag: true, - ValidArgsFunction: cli.validBouncerID, - RunE: func(cmd *cobra.Command, args []string) error { - bouncerName := args[0] - - b, err := cli.db.Ent.Bouncer.Query(). - Where(bouncer.Name(bouncerName)). - Only(cmd.Context()) - if err != nil { - return fmt.Errorf("unable to read bouncer data '%s': %w", bouncerName, err) - } - - return cli.inspect(b) - }, - } - - return cmd -} diff --git a/cmd/crowdsec-cli/clibouncer/delete.go b/cmd/crowdsec-cli/clibouncer/delete.go new file mode 100644 index 00000000000..6e2f312d4af --- /dev/null +++ b/cmd/crowdsec-cli/clibouncer/delete.go @@ -0,0 +1,51 @@ +package clibouncer + +import ( + "context" + "errors" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/database" +) + +func (cli *cliBouncers) delete(ctx context.Context, bouncers []string, ignoreMissing bool) error { + for _, bouncerID := range bouncers { + if err := cli.db.DeleteBouncer(ctx, bouncerID); err != nil { + var notFoundErr *database.BouncerNotFoundError + if ignoreMissing && errors.As(err, ¬FoundErr) { + return nil + } + + return fmt.Errorf("unable to delete bouncer: %w", err) + } + + log.Infof("bouncer '%s' deleted successfully", bouncerID) + } + + return nil +} + +func (cli *cliBouncers) newDeleteCmd() *cobra.Command { + var ignoreMissing bool + + cmd := &cobra.Command{ + Use: "delete MyBouncerName", + Short: "delete bouncer(s) from the database", + Example: `cscli bouncers delete "bouncer1" "bouncer2"`, + Args: cobra.MinimumNArgs(1), + Aliases: []string{"remove"}, + DisableAutoGenTag: true, + ValidArgsFunction: cli.validBouncerID, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.delete(cmd.Context(), args, ignoreMissing) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&ignoreMissing, "ignore-missing", false, "don't print errors if one or more bouncers don't exist") + + return cmd +} diff --git a/cmd/crowdsec-cli/clibouncer/inspect.go b/cmd/crowdsec-cli/clibouncer/inspect.go new file mode 100644 index 00000000000..6dac386b888 --- /dev/null +++ b/cmd/crowdsec-cli/clibouncer/inspect.go @@ -0,0 +1,98 @@ +package clibouncer + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo" + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" + "github.com/crowdsecurity/crowdsec/pkg/database/ent/bouncer" +) + +func (cli *cliBouncers) inspectHuman(out io.Writer, bouncer *ent.Bouncer) { + t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer + + t.SetTitle("Bouncer: " + bouncer.Name) + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + }) + + lastPull := "" + if bouncer.LastPull != nil { + lastPull = bouncer.LastPull.String() + } + + t.AppendRows([]table.Row{ + {"Created At", bouncer.CreatedAt}, + {"Last Update", bouncer.UpdatedAt}, + {"Revoked?", bouncer.Revoked}, + {"IP Address", bouncer.IPAddress}, + {"Type", bouncer.Type}, + {"Version", bouncer.Version}, + {"Last Pull", lastPull}, + {"Auth type", bouncer.AuthType}, + {"OS", clientinfo.GetOSNameAndVersion(bouncer)}, + }) + + for _, ff := range clientinfo.GetFeatureFlagList(bouncer) { + t.AppendRow(table.Row{"Feature Flags", ff}) + } + + io.WriteString(out, t.Render()+"\n") +} + +func (cli *cliBouncers) inspect(bouncer *ent.Bouncer) error { + out := color.Output + outputFormat := cli.cfg().Cscli.Output + + switch outputFormat { + case "human": + cli.inspectHuman(out, bouncer) + case "json": + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(newBouncerInfo(bouncer)); err != nil { + return errors.New("failed to serialize") + } + + return nil + default: + return fmt.Errorf("output format '%s' not supported for this command", outputFormat) + } + + return nil +} + +func (cli *cliBouncers) newInspectCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect [bouncer_name]", + Short: "inspect a bouncer by name", + Example: `cscli bouncers inspect "bouncer1"`, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + ValidArgsFunction: cli.validBouncerID, + RunE: func(cmd *cobra.Command, args []string) error { + bouncerName := args[0] + + b, err := cli.db.Ent.Bouncer.Query(). + Where(bouncer.Name(bouncerName)). + Only(cmd.Context()) + if err != nil { + return fmt.Errorf("unable to read bouncer data '%s': %w", bouncerName, err) + } + + return cli.inspect(b) + }, + } + + return cmd +} diff --git a/cmd/crowdsec-cli/clibouncer/list.go b/cmd/crowdsec-cli/clibouncer/list.go new file mode 100644 index 00000000000..a13ca994e1e --- /dev/null +++ b/cmd/crowdsec-cli/clibouncer/list.go @@ -0,0 +1,117 @@ +package clibouncer + +import ( + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "time" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" + "github.com/crowdsecurity/crowdsec/pkg/database" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" + "github.com/crowdsecurity/crowdsec/pkg/emoji" +) + +func (cli *cliBouncers) listHuman(out io.Writer, bouncers ent.Bouncers) { + t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer + t.AppendHeader(table.Row{"Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type"}) + + for _, b := range bouncers { + revoked := emoji.CheckMark + if b.Revoked { + revoked = emoji.Prohibited + } + + lastPull := "" + if b.LastPull != nil { + lastPull = b.LastPull.Format(time.RFC3339) + } + + t.AppendRow(table.Row{b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType}) + } + + io.WriteString(out, t.Render()+"\n") +} + +func (cli *cliBouncers) listCSV(out io.Writer, bouncers ent.Bouncers) error { + 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" + } + + lastPull := "" + if b.LastPull != nil { + lastPull = b.LastPull.Format(time.RFC3339) + } + + if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil { + return fmt.Errorf("failed to write raw: %w", err) + } + } + + csvwriter.Flush() + + return nil +} + +func (cli *cliBouncers) List(ctx context.Context, out io.Writer, db *database.Client) error { + // XXX: must use the provided db object, the one in the struct might be nil + // (calling List directly skips the PersistentPreRunE) + + bouncers, err := db.ListBouncers(ctx) + if err != nil { + return fmt.Errorf("unable to list bouncers: %w", err) + } + + switch cli.cfg().Cscli.Output { + case "human": + cli.listHuman(out, bouncers) + case "json": + info := make([]bouncerInfo, 0, len(bouncers)) + for _, b := range bouncers { + info = append(info, newBouncerInfo(b)) + } + + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(info); err != nil { + return errors.New("failed to serialize") + } + + return nil + case "raw": + return cli.listCSV(out, bouncers) + } + + return nil +} + +func (cli *cliBouncers) newListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all bouncers within the database", + Example: `cscli bouncers list`, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.List(cmd.Context(), color.Output, cli.db) + }, + } + + return cmd +} diff --git a/cmd/crowdsec-cli/clibouncer/prune.go b/cmd/crowdsec-cli/clibouncer/prune.go new file mode 100644 index 00000000000..754e0898a3b --- /dev/null +++ b/cmd/crowdsec-cli/clibouncer/prune.go @@ -0,0 +1,85 @@ +package clibouncer + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask" +) + +func (cli *cliBouncers) prune(ctx context.Context, duration time.Duration, force bool) error { + if duration < 2*time.Minute { + if yes, err := ask.YesNo( + "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.QueryBouncersInactiveSince(ctx, 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 + } + + cli.listHuman(color.Output, bouncers) + + if !force { + if yes, err := ask.YesNo( + "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(ctx, bouncers) + if err != nil { + return fmt.Errorf("unable to prune bouncers: %w", 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{ + Use: "prune", + Short: "prune multiple bouncers from the database", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + Example: `cscli bouncers prune -d 45m +cscli bouncers prune -d 45m --force`, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.prune(cmd.Context(), duration, force) + }, + } + + 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 +} From c8a7ab0c7fc1f69e89006d4eac6f22fd78db80e6 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 30 Oct 2024 00:18:53 +0100 Subject: [PATCH 4/6] refact cscli machines --- cmd/crowdsec-cli/climachine/add.go | 152 ++++++ cmd/crowdsec-cli/climachine/delete.go | 52 +++ cmd/crowdsec-cli/climachine/inspect.go | 184 ++++++++ cmd/crowdsec-cli/climachine/list.go | 137 ++++++ cmd/crowdsec-cli/climachine/machines.go | 585 ------------------------ cmd/crowdsec-cli/climachine/prune.go | 96 ++++ cmd/crowdsec-cli/climachine/validate.go | 35 ++ 7 files changed, 656 insertions(+), 585 deletions(-) create mode 100644 cmd/crowdsec-cli/climachine/add.go create mode 100644 cmd/crowdsec-cli/climachine/delete.go create mode 100644 cmd/crowdsec-cli/climachine/inspect.go create mode 100644 cmd/crowdsec-cli/climachine/list.go create mode 100644 cmd/crowdsec-cli/climachine/prune.go create mode 100644 cmd/crowdsec-cli/climachine/validate.go diff --git a/cmd/crowdsec-cli/climachine/add.go b/cmd/crowdsec-cli/climachine/add.go new file mode 100644 index 00000000000..afddb4e4b65 --- /dev/null +++ b/cmd/crowdsec-cli/climachine/add.go @@ -0,0 +1,152 @@ +package climachine + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/go-openapi/strfmt" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/types" +) + +func (cli *cliMachines) add(ctx context.Context, 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 + if len(args) == 0 { + if !autoAdd { + return errors.New("please specify a machine name to add, or use --auto") + } + + machineID, err = idgen.GenerateMachineID("") + if err != nil { + return fmt.Errorf("unable to generate machine id: %w", err) + } + } else { + machineID = args[0] + } + + clientCfg := cli.cfg().API.Client + serverCfg := cli.cfg().API.Server + + /*check if file already exists*/ + if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" { + credFile := clientCfg.CredentialsFilePath + // use the default only if the file does not exist + _, err = os.Stat(credFile) + + switch { + case os.IsNotExist(err) || force: + dumpFile = credFile + case err != nil: + return fmt.Errorf("unable to stat '%s': %w", credFile, err) + default: + return fmt.Errorf(`credentials file '%s' already exists: please remove it, use "--force" or specify a different file with "-f" ("-f -" for standard output)`, credFile) + } + } + + if dumpFile == "" { + return errors.New(`please specify a file to dump credentials to, with -f ("-f -" for standard output)`) + } + + // create a password if it's not specified by user + if machinePassword == "" && !interactive { + if !autoAdd { + return errors.New("please specify a password with --password or use --auto") + } + + machinePassword = idgen.GeneratePassword(idgen.PasswordLength) + } else if machinePassword == "" && interactive { + qs := &survey.Password{ + Message: "Please provide a password for the machine:", + } + survey.AskOne(qs, &machinePassword) + } + + password := strfmt.Password(machinePassword) + + _, err = cli.db.CreateMachine(ctx, &machineID, &password, "", true, force, types.PasswordAuthType) + if err != nil { + return fmt.Errorf("unable to create machine: %w", err) + } + + fmt.Fprintf(os.Stderr, "Machine '%s' successfully added to the local API.\n", machineID) + + if apiURL == "" { + if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" { + apiURL = clientCfg.Credentials.URL + } else if serverCfg.ClientURL() != "" { + apiURL = serverCfg.ClientURL() + } else { + return errors.New("unable to dump an api URL. Please provide it in your configuration or with the -u parameter") + } + } + + apiCfg := csconfig.ApiCredentialsCfg{ + Login: machineID, + Password: password.String(), + URL: apiURL, + } + + apiConfigDump, err := yaml.Marshal(apiCfg) + if err != nil { + return fmt.Errorf("unable to serialize api credentials: %w", err) + } + + if dumpFile != "" && dumpFile != "-" { + if err = os.WriteFile(dumpFile, apiConfigDump, 0o600); err != nil { + return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err) + } + + fmt.Fprintf(os.Stderr, "API credentials written to '%s'.\n", dumpFile) + } else { + fmt.Print(string(apiConfigDump)) + } + + return nil +} + +func (cli *cliMachines) newAddCmd() *cobra.Command { + var ( + password MachinePassword + dumpFile string + apiURL string + interactive bool + autoAdd bool + force bool + ) + + cmd := &cobra.Command{ + Use: "add", + Short: "add a single machine to the database", + DisableAutoGenTag: true, + Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`, + Example: `cscli machines add --auto +cscli machines add MyTestMachine --auto +cscli machines add MyTestMachine --password MyPassword +cscli machines add -f- --auto > /tmp/mycreds.yaml`, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.add(cmd.Context(), args, string(password), dumpFile, apiURL, interactive, autoAdd, force) + }, + } + + flags := cmd.Flags() + 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 +} diff --git a/cmd/crowdsec-cli/climachine/delete.go b/cmd/crowdsec-cli/climachine/delete.go new file mode 100644 index 00000000000..644ce93c642 --- /dev/null +++ b/cmd/crowdsec-cli/climachine/delete.go @@ -0,0 +1,52 @@ +package climachine + +import ( + "context" + "errors" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/database" +) + +func (cli *cliMachines) delete(ctx context.Context, machines []string, ignoreMissing bool) error { + for _, machineID := range machines { + if err := cli.db.DeleteWatcher(ctx, machineID); err != nil { + var notFoundErr *database.MachineNotFoundError + if ignoreMissing && errors.As(err, ¬FoundErr) { + return nil + } + + log.Errorf("unable to delete machine: %s", err) + + return nil + } + + log.Infof("machine '%s' deleted successfully", machineID) + } + + return nil +} + +func (cli *cliMachines) newDeleteCmd() *cobra.Command { + var ignoreMissing bool + + cmd := &cobra.Command{ + Use: "delete [machine_name]...", + Short: "delete machine(s) by name", + Example: `cscli machines delete "machine1" "machine2"`, + Args: cobra.MinimumNArgs(1), + Aliases: []string{"remove"}, + DisableAutoGenTag: true, + ValidArgsFunction: cli.validMachineID, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.delete(cmd.Context(), args, ignoreMissing) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&ignoreMissing, "ignore-missing", false, "don't print errors if one or more machines don't exist") + + return cmd +} diff --git a/cmd/crowdsec-cli/climachine/inspect.go b/cmd/crowdsec-cli/climachine/inspect.go new file mode 100644 index 00000000000..b08f2f62794 --- /dev/null +++ b/cmd/crowdsec-cli/climachine/inspect.go @@ -0,0 +1,184 @@ +package climachine + +import ( + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo" + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" + "github.com/crowdsecurity/crowdsec/pkg/cwhub" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" +) + +func (cli *cliMachines) inspectHubHuman(out io.Writer, machine *ent.Machine) { + state := machine.Hubstate + + if len(state) == 0 { + fmt.Println("No hub items found for this machine") + return + } + + // group state rows by type for multiple tables + rowsByType := make(map[string][]table.Row) + + for itemType, items := range state { + for _, item := range items { + if _, ok := rowsByType[itemType]; !ok { + rowsByType[itemType] = make([]table.Row, 0) + } + + row := table.Row{item.Name, item.Status, item.Version} + rowsByType[itemType] = append(rowsByType[itemType], row) + } + } + + for itemType, rows := range rowsByType { + t := cstable.New(out, cli.cfg().Cscli.Color).Writer + t.AppendHeader(table.Row{"Name", "Status", "Version"}) + t.SetTitle(itemType) + t.AppendRows(rows) + io.WriteString(out, t.Render()+"\n") + } +} + +func (cli *cliMachines) inspectHuman(out io.Writer, machine *ent.Machine) { + t := cstable.New(out, cli.cfg().Cscli.Color).Writer + + t.SetTitle("Machine: " + machine.MachineId) + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, AutoMerge: true}, + }) + + t.AppendRows([]table.Row{ + {"IP Address", machine.IpAddress}, + {"Created At", machine.CreatedAt}, + {"Last Update", machine.UpdatedAt}, + {"Last Heartbeat", machine.LastHeartbeat}, + {"Validated?", machine.IsValidated}, + {"CrowdSec version", machine.Version}, + {"OS", clientinfo.GetOSNameAndVersion(machine)}, + {"Auth type", machine.AuthType}, + }) + + for dsName, dsCount := range machine.Datasources { + t.AppendRow(table.Row{"Datasources", fmt.Sprintf("%s: %d", dsName, dsCount)}) + } + + for _, ff := range clientinfo.GetFeatureFlagList(machine) { + t.AppendRow(table.Row{"Feature Flags", ff}) + } + + for _, coll := range machine.Hubstate[cwhub.COLLECTIONS] { + t.AppendRow(table.Row{"Collections", coll.Name}) + } + + io.WriteString(out, t.Render()+"\n") +} + +func (cli *cliMachines) inspect(machine *ent.Machine) error { + out := color.Output + outputFormat := cli.cfg().Cscli.Output + + switch outputFormat { + case "human": + cli.inspectHuman(out, machine) + case "json": + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(newMachineInfo(machine)); err != nil { + return errors.New("failed to serialize") + } + + return nil + default: + return fmt.Errorf("output format '%s' not supported for this command", outputFormat) + } + + return nil +} + +func (cli *cliMachines) inspectHub(machine *ent.Machine) error { + out := color.Output + + switch cli.cfg().Cscli.Output { + case "human": + cli.inspectHubHuman(out, machine) + case "json": + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(machine.Hubstate); err != nil { + return errors.New("failed to serialize") + } + + return nil + case "raw": + csvwriter := csv.NewWriter(out) + + err := csvwriter.Write([]string{"type", "name", "status", "version"}) + if err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + rows := make([][]string, 0) + + for itemType, items := range machine.Hubstate { + for _, item := range items { + rows = append(rows, []string{itemType, item.Name, item.Status, item.Version}) + } + } + + for _, row := range rows { + if err := csvwriter.Write(row); err != nil { + return fmt.Errorf("failed to write raw output: %w", err) + } + } + + csvwriter.Flush() + } + + return nil +} + +func (cli *cliMachines) newInspectCmd() *cobra.Command { + var showHub bool + + cmd := &cobra.Command{ + Use: "inspect [machine_name]", + Short: "inspect a machine by name", + Example: `cscli machines inspect "machine1"`, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + ValidArgsFunction: cli.validMachineID, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + machineID := args[0] + + machine, err := cli.db.QueryMachineByID(ctx, machineID) + if err != nil { + return fmt.Errorf("unable to read machine data '%s': %w", machineID, err) + } + + if showHub { + return cli.inspectHub(machine) + } + + return cli.inspect(machine) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&showHub, "hub", "H", false, "show hub state") + + return cmd +} diff --git a/cmd/crowdsec-cli/climachine/list.go b/cmd/crowdsec-cli/climachine/list.go new file mode 100644 index 00000000000..6bedb2ad807 --- /dev/null +++ b/cmd/crowdsec-cli/climachine/list.go @@ -0,0 +1,137 @@ +package climachine + +import ( + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "time" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo" + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" + "github.com/crowdsecurity/crowdsec/pkg/database" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" + "github.com/crowdsecurity/crowdsec/pkg/emoji" +) + +// getLastHeartbeat returns the last heartbeat timestamp of a machine +// and a boolean indicating if the machine is considered active or not. +func getLastHeartbeat(m *ent.Machine) (string, bool) { + if m.LastHeartbeat == nil { + return "-", false + } + + elapsed := time.Now().UTC().Sub(*m.LastHeartbeat) + + hb := elapsed.Truncate(time.Second).String() + if elapsed > 2*time.Minute { + return hb, false + } + + return hb, true +} + +func (cli *cliMachines) listHuman(out io.Writer, machines ent.Machines) { + t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer + t.AppendHeader(table.Row{"Name", "IP Address", "Last Update", "Status", "Version", "OS", "Auth Type", "Last Heartbeat"}) + + for _, m := range machines { + validated := emoji.Prohibited + if m.IsValidated { + validated = emoji.CheckMark + } + + hb, active := getLastHeartbeat(m) + if !active { + hb = emoji.Warning + " " + hb + } + + t.AppendRow(table.Row{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, clientinfo.GetOSNameAndVersion(m), m.AuthType, hb}) + } + + io.WriteString(out, t.Render()+"\n") +} + +func (cli *cliMachines) listCSV(out io.Writer, machines ent.Machines) error { + csvwriter := csv.NewWriter(out) + + err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat", "os"}) + if err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + for _, m := range machines { + validated := "false" + if m.IsValidated { + validated = "true" + } + + hb := "-" + if m.LastHeartbeat != nil { + hb = m.LastHeartbeat.Format(time.RFC3339) + } + + if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb, fmt.Sprintf("%s/%s", m.Osname, m.Osversion)}); err != nil { + return fmt.Errorf("failed to write raw output: %w", err) + } + } + + csvwriter.Flush() + + return nil +} + +func (cli *cliMachines) List(ctx context.Context, out io.Writer, db *database.Client) error { + // XXX: must use the provided db object, the one in the struct might be nil + // (calling List directly skips the PersistentPreRunE) + + machines, err := db.ListMachines(ctx) + if err != nil { + return fmt.Errorf("unable to list machines: %w", err) + } + + switch cli.cfg().Cscli.Output { + case "human": + cli.listHuman(out, machines) + case "json": + info := make([]machineInfo, 0, len(machines)) + for _, m := range machines { + info = append(info, newMachineInfo(m)) + } + + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + + if err := enc.Encode(info); err != nil { + return errors.New("failed to serialize") + } + + return nil + case "raw": + return cli.listCSV(out, machines) + } + + return nil +} + +func (cli *cliMachines) newListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all machines in the database", + Long: `list all machines in the database with their status and last heartbeat`, + Example: `cscli machines list`, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.List(cmd.Context(), color.Output, cli.db) + }, + } + + return cmd +} diff --git a/cmd/crowdsec-cli/climachine/machines.go b/cmd/crowdsec-cli/climachine/machines.go index 1fbedcf57fd..ad503c6e936 100644 --- a/cmd/crowdsec-cli/climachine/machines.go +++ b/cmd/crowdsec-cli/climachine/machines.go @@ -1,55 +1,19 @@ package climachine import ( - "context" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - "os" "slices" "strings" "time" - "github.com/AlecAivazis/survey/v2" - "github.com/fatih/color" - "github.com/go-openapi/strfmt" - "github.com/jedib0t/go-pretty/v6/table" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" "github.com/crowdsecurity/crowdsec/pkg/csconfig" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/database/ent" - "github.com/crowdsecurity/crowdsec/pkg/emoji" - "github.com/crowdsecurity/crowdsec/pkg/types" ) -// getLastHeartbeat returns the last heartbeat timestamp of a machine -// and a boolean indicating if the machine is considered active or not. -func getLastHeartbeat(m *ent.Machine) (string, bool) { - if m.LastHeartbeat == nil { - return "-", false - } - - elapsed := time.Now().UTC().Sub(*m.LastHeartbeat) - - hb := elapsed.Truncate(time.Second).String() - if elapsed > 2*time.Minute { - return hb, false - } - - return hb, true -} - type configGetter = func() *csconfig.Config type cliMachines struct { @@ -97,58 +61,6 @@ Note: This command requires database direct access, so is intended to be run on return cmd } -func (cli *cliMachines) inspectHubHuman(out io.Writer, machine *ent.Machine) { - state := machine.Hubstate - - if len(state) == 0 { - fmt.Println("No hub items found for this machine") - return - } - - // group state rows by type for multiple tables - rowsByType := make(map[string][]table.Row) - - for itemType, items := range state { - for _, item := range items { - if _, ok := rowsByType[itemType]; !ok { - rowsByType[itemType] = make([]table.Row, 0) - } - - row := table.Row{item.Name, item.Status, item.Version} - rowsByType[itemType] = append(rowsByType[itemType], row) - } - } - - for itemType, rows := range rowsByType { - t := cstable.New(out, cli.cfg().Cscli.Color).Writer - t.AppendHeader(table.Row{"Name", "Status", "Version"}) - t.SetTitle(itemType) - t.AppendRows(rows) - io.WriteString(out, t.Render()+"\n") - } -} - -func (cli *cliMachines) listHuman(out io.Writer, machines ent.Machines) { - t := cstable.NewLight(out, cli.cfg().Cscli.Color).Writer - t.AppendHeader(table.Row{"Name", "IP Address", "Last Update", "Status", "Version", "OS", "Auth Type", "Last Heartbeat"}) - - for _, m := range machines { - validated := emoji.Prohibited - if m.IsValidated { - validated = emoji.CheckMark - } - - hb, active := getLastHeartbeat(m) - if !active { - hb = emoji.Warning + " " + hb - } - - t.AppendRow(table.Row{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, clientinfo.GetOSNameAndVersion(m), m.AuthType, hb}) - } - - io.WriteString(out, t.Render()+"\n") -} - // machineInfo contains only the data we want for inspect/list: no hub status, scenarios, edges, etc. type machineInfo struct { CreatedAt time.Time `json:"created_at,omitempty"` @@ -182,219 +94,6 @@ func newMachineInfo(m *ent.Machine) machineInfo { } } -func (cli *cliMachines) listCSV(out io.Writer, machines ent.Machines) error { - csvwriter := csv.NewWriter(out) - - err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat", "os"}) - if err != nil { - return fmt.Errorf("failed to write header: %w", err) - } - - for _, m := range machines { - validated := "false" - if m.IsValidated { - validated = "true" - } - - hb := "-" - if m.LastHeartbeat != nil { - hb = m.LastHeartbeat.Format(time.RFC3339) - } - - if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb, fmt.Sprintf("%s/%s", m.Osname, m.Osversion)}); err != nil { - return fmt.Errorf("failed to write raw output: %w", err) - } - } - - csvwriter.Flush() - - return nil -} - -func (cli *cliMachines) List(ctx context.Context, out io.Writer, db *database.Client) error { - // XXX: must use the provided db object, the one in the struct might be nil - // (calling List directly skips the PersistentPreRunE) - - machines, err := db.ListMachines(ctx) - if err != nil { - return fmt.Errorf("unable to list machines: %w", err) - } - - switch cli.cfg().Cscli.Output { - case "human": - cli.listHuman(out, machines) - case "json": - info := make([]machineInfo, 0, len(machines)) - for _, m := range machines { - info = append(info, newMachineInfo(m)) - } - - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - - if err := enc.Encode(info); err != nil { - return errors.New("failed to serialize") - } - - return nil - case "raw": - return cli.listCSV(out, machines) - } - - return nil -} - -func (cli *cliMachines) newListCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Short: "list all machines in the database", - Long: `list all machines in the database with their status and last heartbeat`, - Example: `cscli machines list`, - Args: cobra.NoArgs, - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return cli.List(cmd.Context(), color.Output, cli.db) - }, - } - - return cmd -} - -func (cli *cliMachines) newAddCmd() *cobra.Command { - var ( - password MachinePassword - dumpFile string - apiURL string - interactive bool - autoAdd bool - force bool - ) - - cmd := &cobra.Command{ - Use: "add", - Short: "add a single machine to the database", - DisableAutoGenTag: true, - Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`, - Example: `cscli machines add --auto -cscli machines add MyTestMachine --auto -cscli machines add MyTestMachine --password MyPassword -cscli machines add -f- --auto > /tmp/mycreds.yaml`, - RunE: func(cmd *cobra.Command, args []string) error { - return cli.add(cmd.Context(), args, string(password), dumpFile, apiURL, interactive, autoAdd, force) - }, - } - - flags := cmd.Flags() - 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 -} - -func (cli *cliMachines) add(ctx context.Context, 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 - if len(args) == 0 { - if !autoAdd { - return errors.New("please specify a machine name to add, or use --auto") - } - - machineID, err = idgen.GenerateMachineID("") - if err != nil { - return fmt.Errorf("unable to generate machine id: %w", err) - } - } else { - machineID = args[0] - } - - clientCfg := cli.cfg().API.Client - serverCfg := cli.cfg().API.Server - - /*check if file already exists*/ - if dumpFile == "" && clientCfg != nil && clientCfg.CredentialsFilePath != "" { - credFile := clientCfg.CredentialsFilePath - // use the default only if the file does not exist - _, err = os.Stat(credFile) - - switch { - case os.IsNotExist(err) || force: - dumpFile = credFile - case err != nil: - return fmt.Errorf("unable to stat '%s': %w", credFile, err) - default: - return fmt.Errorf(`credentials file '%s' already exists: please remove it, use "--force" or specify a different file with "-f" ("-f -" for standard output)`, credFile) - } - } - - if dumpFile == "" { - return errors.New(`please specify a file to dump credentials to, with -f ("-f -" for standard output)`) - } - - // create a password if it's not specified by user - if machinePassword == "" && !interactive { - if !autoAdd { - return errors.New("please specify a password with --password or use --auto") - } - - machinePassword = idgen.GeneratePassword(idgen.PasswordLength) - } else if machinePassword == "" && interactive { - qs := &survey.Password{ - Message: "Please provide a password for the machine:", - } - survey.AskOne(qs, &machinePassword) - } - - password := strfmt.Password(machinePassword) - - _, err = cli.db.CreateMachine(ctx, &machineID, &password, "", true, force, types.PasswordAuthType) - if err != nil { - return fmt.Errorf("unable to create machine: %w", err) - } - - fmt.Fprintf(os.Stderr, "Machine '%s' successfully added to the local API.\n", machineID) - - if apiURL == "" { - if clientCfg != nil && clientCfg.Credentials != nil && clientCfg.Credentials.URL != "" { - apiURL = clientCfg.Credentials.URL - } else if serverCfg.ClientURL() != "" { - apiURL = serverCfg.ClientURL() - } else { - return errors.New("unable to dump an api URL. Please provide it in your configuration or with the -u parameter") - } - } - - apiCfg := csconfig.ApiCredentialsCfg{ - Login: machineID, - Password: password.String(), - URL: apiURL, - } - - apiConfigDump, err := yaml.Marshal(apiCfg) - if err != nil { - return fmt.Errorf("unable to serialize api credentials: %w", err) - } - - if dumpFile != "" && dumpFile != "-" { - if err = os.WriteFile(dumpFile, apiConfigDump, 0o600); err != nil { - return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err) - } - - fmt.Fprintf(os.Stderr, "API credentials written to '%s'.\n", dumpFile) - } else { - fmt.Print(string(apiConfigDump)) - } - - return nil -} - // validMachineID returns a list of machine IDs for command completion func (cli *cliMachines) validMachineID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var err error @@ -431,287 +130,3 @@ func (cli *cliMachines) validMachineID(cmd *cobra.Command, args []string, toComp return ret, cobra.ShellCompDirectiveNoFileComp } - -func (cli *cliMachines) delete(ctx context.Context, machines []string, ignoreMissing bool) error { - for _, machineID := range machines { - if err := cli.db.DeleteWatcher(ctx, machineID); err != nil { - var notFoundErr *database.MachineNotFoundError - if ignoreMissing && errors.As(err, ¬FoundErr) { - return nil - } - - log.Errorf("unable to delete machine: %s", err) - - return nil - } - - log.Infof("machine '%s' deleted successfully", machineID) - } - - return nil -} - -func (cli *cliMachines) newDeleteCmd() *cobra.Command { - var ignoreMissing bool - - cmd := &cobra.Command{ - Use: "delete [machine_name]...", - Short: "delete machine(s) by name", - Example: `cscli machines delete "machine1" "machine2"`, - Args: cobra.MinimumNArgs(1), - Aliases: []string{"remove"}, - DisableAutoGenTag: true, - ValidArgsFunction: cli.validMachineID, - RunE: func(cmd *cobra.Command, args []string) error { - return cli.delete(cmd.Context(), args, ignoreMissing) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&ignoreMissing, "ignore-missing", false, "don't print errors if one or more machines don't exist") - - return cmd -} - -func (cli *cliMachines) prune(ctx context.Context, duration time.Duration, notValidOnly bool, force bool) error { - if duration < 2*time.Minute && !notValidOnly { - if yes, err := ask.YesNo( - "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 - } - } - - machines := []*ent.Machine{} - if pending, err := cli.db.QueryPendingMachine(ctx); err == nil { - machines = append(machines, pending...) - } - - if !notValidOnly { - if pending, err := cli.db.QueryMachinesInactiveSince(ctx, time.Now().UTC().Add(-duration)); err == nil { - machines = append(machines, pending...) - } - } - - if len(machines) == 0 { - fmt.Println("No machines to prune.") - return nil - } - - cli.listHuman(color.Output, machines) - - if !force { - if yes, err := ask.YesNo( - "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(ctx, machines) - if err != nil { - return fmt.Errorf("unable to prune machines: %w", err) - } - - fmt.Fprintf(os.Stderr, "successfully deleted %d machines\n", deleted) - - return nil -} - -func (cli *cliMachines) newPruneCmd() *cobra.Command { - var ( - duration time.Duration - notValidOnly bool - force bool - ) - - const defaultDuration = 10 * time.Minute - - cmd := &cobra.Command{ - Use: "prune", - Short: "prune multiple machines from the database", - Long: `prune multiple machines that are not validated or have not connected to the local API in a given duration.`, - Example: `cscli machines prune -cscli machines prune --duration 1h -cscli machines prune --not-validated-only --force`, - Args: cobra.NoArgs, - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return cli.prune(cmd.Context(), duration, notValidOnly, force) - }, - } - - flags := cmd.Flags() - flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat") - flags.BoolVar(¬ValidOnly, "not-validated-only", false, "only prune machines that are not validated") - flags.BoolVar(&force, "force", false, "force prune without asking for confirmation") - - return cmd -} - -func (cli *cliMachines) validate(ctx context.Context, machineID string) error { - if err := cli.db.ValidateMachine(ctx, machineID); err != nil { - return fmt.Errorf("unable to validate machine '%s': %w", machineID, err) - } - - log.Infof("machine '%s' validated successfully", machineID) - - return nil -} - -func (cli *cliMachines) newValidateCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "validate", - Short: "validate a machine to access the local API", - Long: `validate a machine to access the local API.`, - Example: `cscli machines validate "machine_name"`, - Args: cobra.ExactArgs(1), - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, args []string) error { - return cli.validate(cmd.Context(), args[0]) - }, - } - - return cmd -} - -func (cli *cliMachines) inspectHuman(out io.Writer, machine *ent.Machine) { - t := cstable.New(out, cli.cfg().Cscli.Color).Writer - - t.SetTitle("Machine: " + machine.MachineId) - - t.SetColumnConfigs([]table.ColumnConfig{ - {Number: 1, AutoMerge: true}, - }) - - t.AppendRows([]table.Row{ - {"IP Address", machine.IpAddress}, - {"Created At", machine.CreatedAt}, - {"Last Update", machine.UpdatedAt}, - {"Last Heartbeat", machine.LastHeartbeat}, - {"Validated?", machine.IsValidated}, - {"CrowdSec version", machine.Version}, - {"OS", clientinfo.GetOSNameAndVersion(machine)}, - {"Auth type", machine.AuthType}, - }) - - for dsName, dsCount := range machine.Datasources { - t.AppendRow(table.Row{"Datasources", fmt.Sprintf("%s: %d", dsName, dsCount)}) - } - - for _, ff := range clientinfo.GetFeatureFlagList(machine) { - t.AppendRow(table.Row{"Feature Flags", ff}) - } - - for _, coll := range machine.Hubstate[cwhub.COLLECTIONS] { - t.AppendRow(table.Row{"Collections", coll.Name}) - } - - io.WriteString(out, t.Render()+"\n") -} - -func (cli *cliMachines) inspect(machine *ent.Machine) error { - out := color.Output - outputFormat := cli.cfg().Cscli.Output - - switch outputFormat { - case "human": - cli.inspectHuman(out, machine) - case "json": - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - - if err := enc.Encode(newMachineInfo(machine)); err != nil { - return errors.New("failed to serialize") - } - - return nil - default: - return fmt.Errorf("output format '%s' not supported for this command", outputFormat) - } - - return nil -} - -func (cli *cliMachines) inspectHub(machine *ent.Machine) error { - out := color.Output - - switch cli.cfg().Cscli.Output { - case "human": - cli.inspectHubHuman(out, machine) - case "json": - enc := json.NewEncoder(out) - enc.SetIndent("", " ") - - if err := enc.Encode(machine.Hubstate); err != nil { - return errors.New("failed to serialize") - } - - return nil - case "raw": - csvwriter := csv.NewWriter(out) - - err := csvwriter.Write([]string{"type", "name", "status", "version"}) - if err != nil { - return fmt.Errorf("failed to write header: %w", err) - } - - rows := make([][]string, 0) - - for itemType, items := range machine.Hubstate { - for _, item := range items { - rows = append(rows, []string{itemType, item.Name, item.Status, item.Version}) - } - } - - for _, row := range rows { - if err := csvwriter.Write(row); err != nil { - return fmt.Errorf("failed to write raw output: %w", err) - } - } - - csvwriter.Flush() - } - - return nil -} - -func (cli *cliMachines) newInspectCmd() *cobra.Command { - var showHub bool - - cmd := &cobra.Command{ - Use: "inspect [machine_name]", - Short: "inspect a machine by name", - Example: `cscli machines inspect "machine1"`, - Args: cobra.ExactArgs(1), - DisableAutoGenTag: true, - ValidArgsFunction: cli.validMachineID, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - machineID := args[0] - - machine, err := cli.db.QueryMachineByID(ctx, machineID) - if err != nil { - return fmt.Errorf("unable to read machine data '%s': %w", machineID, err) - } - - if showHub { - return cli.inspectHub(machine) - } - - return cli.inspect(machine) - }, - } - - flags := cmd.Flags() - - flags.BoolVarP(&showHub, "hub", "H", false, "show hub state") - - return cmd -} diff --git a/cmd/crowdsec-cli/climachine/prune.go b/cmd/crowdsec-cli/climachine/prune.go new file mode 100644 index 00000000000..ed41ef0a736 --- /dev/null +++ b/cmd/crowdsec-cli/climachine/prune.go @@ -0,0 +1,96 @@ +package climachine + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" +) + +func (cli *cliMachines) prune(ctx context.Context, duration time.Duration, notValidOnly bool, force bool) error { + if duration < 2*time.Minute && !notValidOnly { + if yes, err := ask.YesNo( + "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 + } + } + + machines := []*ent.Machine{} + if pending, err := cli.db.QueryPendingMachine(ctx); err == nil { + machines = append(machines, pending...) + } + + if !notValidOnly { + if pending, err := cli.db.QueryMachinesInactiveSince(ctx, time.Now().UTC().Add(-duration)); err == nil { + machines = append(machines, pending...) + } + } + + if len(machines) == 0 { + fmt.Println("No machines to prune.") + return nil + } + + cli.listHuman(color.Output, machines) + + if !force { + if yes, err := ask.YesNo( + "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(ctx, machines) + if err != nil { + return fmt.Errorf("unable to prune machines: %w", err) + } + + fmt.Fprintf(os.Stderr, "successfully deleted %d machines\n", deleted) + + return nil +} + +func (cli *cliMachines) newPruneCmd() *cobra.Command { + var ( + duration time.Duration + notValidOnly bool + force bool + ) + + const defaultDuration = 10 * time.Minute + + cmd := &cobra.Command{ + Use: "prune", + Short: "prune multiple machines from the database", + Long: `prune multiple machines that are not validated or have not connected to the local API in a given duration.`, + Example: `cscli machines prune +cscli machines prune --duration 1h +cscli machines prune --not-validated-only --force`, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.prune(cmd.Context(), duration, notValidOnly, force) + }, + } + + flags := cmd.Flags() + flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat") + flags.BoolVar(¬ValidOnly, "not-validated-only", false, "only prune machines that are not validated") + flags.BoolVar(&force, "force", false, "force prune without asking for confirmation") + + return cmd +} diff --git a/cmd/crowdsec-cli/climachine/validate.go b/cmd/crowdsec-cli/climachine/validate.go new file mode 100644 index 00000000000..cba872aa05d --- /dev/null +++ b/cmd/crowdsec-cli/climachine/validate.go @@ -0,0 +1,35 @@ +package climachine + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func (cli *cliMachines) validate(ctx context.Context, machineID string) error { + if err := cli.db.ValidateMachine(ctx, machineID); err != nil { + return fmt.Errorf("unable to validate machine '%s': %w", machineID, err) + } + + log.Infof("machine '%s' validated successfully", machineID) + + return nil +} + +func (cli *cliMachines) newValidateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "validate", + Short: "validate a machine to access the local API", + Long: `validate a machine to access the local API.`, + Example: `cscli machines validate "machine_name"`, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cli.validate(cmd.Context(), args[0]) + }, + } + + return cmd +} From b7c5c4bf2cf2a13aab50b40d7311bb91d96c9669 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 30 Oct 2024 09:34:11 +0100 Subject: [PATCH 5/6] refact "cscli lapi" --- cmd/crowdsec-cli/clilapi/context.go | 395 ++++++++++++ cmd/crowdsec-cli/clilapi/lapi.go | 594 ------------------ cmd/crowdsec-cli/clilapi/register.go | 117 ++++ cmd/crowdsec-cli/clilapi/status.go | 115 ++++ .../clilapi/{lapi_test.go => status_test.go} | 0 5 files changed, 627 insertions(+), 594 deletions(-) create mode 100644 cmd/crowdsec-cli/clilapi/context.go create mode 100644 cmd/crowdsec-cli/clilapi/register.go create mode 100644 cmd/crowdsec-cli/clilapi/status.go rename cmd/crowdsec-cli/clilapi/{lapi_test.go => status_test.go} (100%) diff --git a/cmd/crowdsec-cli/clilapi/context.go b/cmd/crowdsec-cli/clilapi/context.go new file mode 100644 index 00000000000..20ceb2b9596 --- /dev/null +++ b/cmd/crowdsec-cli/clilapi/context.go @@ -0,0 +1,395 @@ +package clilapi + +import ( + "errors" + "fmt" + "slices" + "sort" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" + "github.com/crowdsecurity/crowdsec/pkg/alertcontext" + "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" + "github.com/crowdsecurity/crowdsec/pkg/parser" +) + +func (cli *cliLapi) addContext(key string, values []string) error { + cfg := cli.cfg() + + if err := alertcontext.ValidateContextExpr(key, values); err != nil { + return fmt.Errorf("invalid context configuration: %w", err) + } + + if _, ok := cfg.Crowdsec.ContextToSend[key]; !ok { + cfg.Crowdsec.ContextToSend[key] = make([]string, 0) + + log.Infof("key '%s' added", key) + } + + data := cfg.Crowdsec.ContextToSend[key] + + for _, val := range values { + if !slices.Contains(data, val) { + log.Infof("value '%s' added to key '%s'", val, key) + data = append(data, val) + } + + cfg.Crowdsec.ContextToSend[key] = data + } + + return cfg.Crowdsec.DumpContextConfigFile() +} + +func (cli *cliLapi) newContextAddCmd() *cobra.Command { + var ( + keyToAdd string + valuesToAdd []string + ) + + cmd := &cobra.Command{ + Use: "add", + Short: "Add context to send with alerts. You must specify the output key with the expr value you want", + Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip +cscli lapi context add --key file_source --value evt.Line.Src +cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user + `, + DisableAutoGenTag: true, + RunE: func(_ *cobra.Command, _ []string) error { + hub, err := require.Hub(cli.cfg(), nil, nil) + if err != nil { + return err + } + + if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil { + return fmt.Errorf("while loading context: %w", err) + } + + if keyToAdd != "" { + return cli.addContext(keyToAdd, valuesToAdd) + } + + for _, v := range valuesToAdd { + keySlice := strings.Split(v, ".") + key := keySlice[len(keySlice)-1] + value := []string{v} + if err := cli.addContext(key, value); err != nil { + return err + } + } + + return nil + }, + } + + flags := cmd.Flags() + flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send") + flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key") + + _ = cmd.MarkFlagRequired("value") + + return cmd +} + +func (cli *cliLapi) newContextStatusCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "List context to send with alerts", + DisableAutoGenTag: true, + RunE: func(_ *cobra.Command, _ []string) error { + cfg := cli.cfg() + hub, err := require.Hub(cfg, nil, nil) + if err != nil { + return err + } + + if err = alertcontext.LoadConsoleContext(cfg, hub); err != nil { + return fmt.Errorf("while loading context: %w", err) + } + + if len(cfg.Crowdsec.ContextToSend) == 0 { + fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.") + return nil + } + + dump, err := yaml.Marshal(cfg.Crowdsec.ContextToSend) + if err != nil { + return fmt.Errorf("unable to show context status: %w", err) + } + + fmt.Print(string(dump)) + + return nil + }, + } + + return cmd +} + +func (cli *cliLapi) newContextDetectCmd() *cobra.Command { + var detectAll bool + + cmd := &cobra.Command{ + Use: "detect", + Short: "Detect available fields from the installed parsers", + Example: `cscli lapi context detect --all +cscli lapi context detect crowdsecurity/sshd-logs + `, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := cli.cfg() + if !detectAll && len(args) == 0 { + _ = cmd.Help() + return errors.New("please provide parsers to detect or --all flag") + } + + // to avoid all the log.Info from the loaders functions + log.SetLevel(log.WarnLevel) + + if err := exprhelpers.Init(nil); err != nil { + return fmt.Errorf("failed to init expr helpers: %w", err) + } + + hub, err := require.Hub(cfg, nil, nil) + if err != nil { + return err + } + + csParsers := parser.NewParsers(hub) + if csParsers, err = parser.LoadParsers(cfg, csParsers); err != nil { + return fmt.Errorf("unable to load parsers: %w", err) + } + + fieldByParsers := make(map[string][]string) + for _, node := range csParsers.Nodes { + if !detectAll && !slices.Contains(args, node.Name) { + continue + } + if !detectAll { + args = removeFromSlice(node.Name, args) + } + fieldByParsers[node.Name] = make([]string, 0) + fieldByParsers[node.Name] = detectNode(node, *csParsers.Ctx) + + subNodeFields := detectSubNode(node, *csParsers.Ctx) + for _, field := range subNodeFields { + if !slices.Contains(fieldByParsers[node.Name], field) { + fieldByParsers[node.Name] = append(fieldByParsers[node.Name], field) + } + } + } + + fmt.Printf("Acquisition :\n\n") + fmt.Printf(" - evt.Line.Module\n") + fmt.Printf(" - evt.Line.Raw\n") + fmt.Printf(" - evt.Line.Src\n") + fmt.Println() + + parsersKey := make([]string, 0) + for k := range fieldByParsers { + parsersKey = append(parsersKey, k) + } + sort.Strings(parsersKey) + + for _, k := range parsersKey { + if len(fieldByParsers[k]) == 0 { + continue + } + fmt.Printf("%s :\n\n", k) + values := fieldByParsers[k] + sort.Strings(values) + for _, value := range values { + fmt.Printf(" - %s\n", value) + } + fmt.Println() + } + + if len(args) > 0 { + for _, parserNotFound := range args { + log.Errorf("parser '%s' not found, can't detect fields", parserNotFound) + } + } + + return nil + }, + } + cmd.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser") + + return cmd +} + +func (cli *cliLapi) newContextDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + DisableAutoGenTag: true, + RunE: func(_ *cobra.Command, _ []string) error { + filePath := cli.cfg().Crowdsec.ConsoleContextPath + if filePath == "" { + filePath = "the context file" + } + + return fmt.Errorf("command 'delete' has been removed, please manually edit %s", filePath) + }, + } + + return cmd +} + +func (cli *cliLapi) newContextCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "context [command]", + Short: "Manage context to send with alerts", + DisableAutoGenTag: true, + PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + cfg := cli.cfg() + if err := cfg.LoadCrowdsec(); err != nil { + fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", cfg.Crowdsec.ConsoleContextPath) + if err.Error() != fileNotFoundMessage { + return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err) + } + } + if cfg.DisableAgent { + return errors.New("agent is disabled and lapi context can only be used on the agent") + } + + return nil + }, + } + + cmd.AddCommand(cli.newContextAddCmd()) + cmd.AddCommand(cli.newContextStatusCmd()) + cmd.AddCommand(cli.newContextDetectCmd()) + cmd.AddCommand(cli.newContextDeleteCmd()) + + return cmd +} + +func detectStaticField(grokStatics []parser.ExtraField) []string { + ret := make([]string, 0) + + for _, static := range grokStatics { + if static.Parsed != "" { + fieldName := "evt.Parsed." + static.Parsed + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + + if static.Meta != "" { + fieldName := "evt.Meta." + static.Meta + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + + if static.TargetByName != "" { + fieldName := static.TargetByName + if !strings.HasPrefix(fieldName, "evt.") { + fieldName = "evt." + fieldName + } + + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + } + + return ret +} + +func detectNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { + ret := make([]string, 0) + + if node.Grok.RunTimeRegexp != nil { + for _, capturedField := range node.Grok.RunTimeRegexp.Names() { + fieldName := "evt.Parsed." + capturedField + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + } + + if node.Grok.RegexpName != "" { + grokCompiled, err := parserCTX.Grok.Get(node.Grok.RegexpName) + // ignore error (parser does not exist?) + if err == nil { + for _, capturedField := range grokCompiled.Names() { + fieldName := "evt.Parsed." + capturedField + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + } + } + + if len(node.Grok.Statics) > 0 { + staticsField := detectStaticField(node.Grok.Statics) + for _, staticField := range staticsField { + if !slices.Contains(ret, staticField) { + ret = append(ret, staticField) + } + } + } + + if len(node.Statics) > 0 { + staticsField := detectStaticField(node.Statics) + for _, staticField := range staticsField { + if !slices.Contains(ret, staticField) { + ret = append(ret, staticField) + } + } + } + + return ret +} + +func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { + ret := make([]string, 0) + + for _, subnode := range node.LeavesNodes { + if subnode.Grok.RunTimeRegexp != nil { + for _, capturedField := range subnode.Grok.RunTimeRegexp.Names() { + fieldName := "evt.Parsed." + capturedField + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + } + + if subnode.Grok.RegexpName != "" { + grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName) + if err == nil { + // ignore error (parser does not exist?) + for _, capturedField := range grokCompiled.Names() { + fieldName := "evt.Parsed." + capturedField + if !slices.Contains(ret, fieldName) { + ret = append(ret, fieldName) + } + } + } + } + + if len(subnode.Grok.Statics) > 0 { + staticsField := detectStaticField(subnode.Grok.Statics) + for _, staticField := range staticsField { + if !slices.Contains(ret, staticField) { + ret = append(ret, staticField) + } + } + } + + if len(subnode.Statics) > 0 { + staticsField := detectStaticField(subnode.Statics) + for _, staticField := range staticsField { + if !slices.Contains(ret, staticField) { + ret = append(ret, staticField) + } + } + } + } + + return ret +} diff --git a/cmd/crowdsec-cli/clilapi/lapi.go b/cmd/crowdsec-cli/clilapi/lapi.go index bb721eefe03..01341330ae8 100644 --- a/cmd/crowdsec-cli/clilapi/lapi.go +++ b/cmd/crowdsec-cli/clilapi/lapi.go @@ -1,36 +1,13 @@ package clilapi import ( - "context" - "errors" "fmt" - "io" - "net/url" - "os" - "slices" - "sort" - "strings" - "github.com/fatih/color" - "github.com/go-openapi/strfmt" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload" - "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" - "github.com/crowdsecurity/crowdsec/pkg/alertcontext" - "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/csconfig" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" - "github.com/crowdsecurity/crowdsec/pkg/models" - "github.com/crowdsecurity/crowdsec/pkg/parser" ) -const LAPIURLPrefix = "v1" - type configGetter = func() *csconfig.Config type cliLapi struct { @@ -43,200 +20,6 @@ func New(cfg configGetter) *cliLapi { } } -// queryLAPIStatus checks if the Local API is reachable, and if the credentials are correct. -func queryLAPIStatus(ctx context.Context, hub *cwhub.Hub, credURL string, login string, password string) (bool, error) { - apiURL, err := url.Parse(credURL) - if err != nil { - return false, err - } - - client, err := apiclient.NewDefaultClient(apiURL, - LAPIURLPrefix, - "", - nil) - if err != nil { - return false, err - } - - pw := strfmt.Password(password) - - itemsForAPI := hub.GetInstalledListForAPI() - - t := models.WatcherAuthRequest{ - MachineID: &login, - Password: &pw, - Scenarios: itemsForAPI, - } - - _, _, err = client.Auth.AuthenticateWatcher(ctx, t) - if err != nil { - return false, err - } - - return true, nil -} - -func (cli *cliLapi) Status(ctx context.Context, out io.Writer, hub *cwhub.Hub) error { - cfg := cli.cfg() - - cred := cfg.API.Client.Credentials - - fmt.Fprintf(out, "Loaded credentials from %s\n", cfg.API.Client.CredentialsFilePath) - fmt.Fprintf(out, "Trying to authenticate with username %s on %s\n", cred.Login, cred.URL) - - _, err := queryLAPIStatus(ctx, hub, cred.URL, cred.Login, cred.Password) - if err != nil { - return fmt.Errorf("failed to authenticate to Local API (LAPI): %w", err) - } - - fmt.Fprintf(out, "You can successfully interact with Local API (LAPI)\n") - - return nil -} - -func (cli *cliLapi) register(ctx context.Context, apiURL string, outputFile string, machine string, token string) error { - var err error - - lapiUser := machine - cfg := cli.cfg() - - if lapiUser == "" { - lapiUser, err = idgen.GenerateMachineID("") - if err != nil { - return fmt.Errorf("unable to generate machine id: %w", err) - } - } - - password := strfmt.Password(idgen.GeneratePassword(idgen.PasswordLength)) - - apiurl, err := prepareAPIURL(cfg.API.Client, apiURL) - if err != nil { - return fmt.Errorf("parsing api url: %w", err) - } - - _, err = apiclient.RegisterClient(ctx, &apiclient.Config{ - MachineID: lapiUser, - Password: password, - RegistrationToken: token, - URL: apiurl, - VersionPrefix: LAPIURLPrefix, - }, nil) - if err != nil { - return fmt.Errorf("api client register: %w", err) - } - - log.Printf("Successfully registered to Local API (LAPI)") - - var dumpFile string - - if outputFile != "" { - dumpFile = outputFile - } else if cfg.API.Client.CredentialsFilePath != "" { - dumpFile = cfg.API.Client.CredentialsFilePath - } else { - dumpFile = "" - } - - apiCfg := cfg.API.Client.Credentials - apiCfg.Login = lapiUser - apiCfg.Password = password.String() - - if apiURL != "" { - apiCfg.URL = apiURL - } - - apiConfigDump, err := yaml.Marshal(apiCfg) - if err != nil { - return fmt.Errorf("unable to serialize api credentials: %w", err) - } - - if dumpFile != "" { - err = os.WriteFile(dumpFile, apiConfigDump, 0o600) - if err != nil { - return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err) - } - - log.Printf("Local API credentials written to '%s'", dumpFile) - } else { - fmt.Printf("%s\n", string(apiConfigDump)) - } - - log.Warning(reload.Message) - - return nil -} - -// prepareAPIURL checks/fixes a LAPI connection url (http, https or socket) and returns an URL struct -func prepareAPIURL(clientCfg *csconfig.LocalApiClientCfg, apiURL string) (*url.URL, error) { - if apiURL == "" { - if clientCfg == nil || clientCfg.Credentials == nil || clientCfg.Credentials.URL == "" { - return nil, errors.New("no Local API URL. Please provide it in your configuration or with the -u parameter") - } - - apiURL = clientCfg.Credentials.URL - } - - // URL needs to end with /, but user doesn't care - if !strings.HasSuffix(apiURL, "/") { - apiURL += "/" - } - - // URL needs to start with http://, but user doesn't care - if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") && !strings.HasPrefix(apiURL, "/") { - apiURL = "http://" + apiURL - } - - return url.Parse(apiURL) -} - -func (cli *cliLapi) newStatusCmd() *cobra.Command { - cmdLapiStatus := &cobra.Command{ - Use: "status", - Short: "Check authentication to Local API (LAPI)", - Args: cobra.MinimumNArgs(0), - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, _ []string) error { - hub, err := require.Hub(cli.cfg(), nil, nil) - if err != nil { - return err - } - - return cli.Status(cmd.Context(), color.Output, hub) - }, - } - - return cmdLapiStatus -} - -func (cli *cliLapi) newRegisterCmd() *cobra.Command { - var ( - apiURL string - outputFile string - machine string - token string - ) - - cmd := &cobra.Command{ - Use: "register", - Short: "Register a machine to Local API (LAPI)", - Long: `Register your machine to the Local API (LAPI). -Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`, - Args: cobra.MinimumNArgs(0), - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return cli.register(cmd.Context(), apiURL, outputFile, machine, token) - }, - } - - flags := cmd.Flags() - flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)") - flags.StringVarP(&outputFile, "file", "f", "", "output file destination") - flags.StringVar(&machine, "machine", "", "Name of the machine to register with") - flags.StringVar(&token, "token", "", "Auto registration token to use") - - return cmd -} - func (cli *cliLapi) NewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "lapi [action]", @@ -257,380 +40,3 @@ func (cli *cliLapi) NewCommand() *cobra.Command { return cmd } - -func (cli *cliLapi) addContext(key string, values []string) error { - cfg := cli.cfg() - - if err := alertcontext.ValidateContextExpr(key, values); err != nil { - return fmt.Errorf("invalid context configuration: %w", err) - } - - if _, ok := cfg.Crowdsec.ContextToSend[key]; !ok { - cfg.Crowdsec.ContextToSend[key] = make([]string, 0) - - log.Infof("key '%s' added", key) - } - - data := cfg.Crowdsec.ContextToSend[key] - - for _, val := range values { - if !slices.Contains(data, val) { - log.Infof("value '%s' added to key '%s'", val, key) - data = append(data, val) - } - - cfg.Crowdsec.ContextToSend[key] = data - } - - return cfg.Crowdsec.DumpContextConfigFile() -} - -func (cli *cliLapi) newContextAddCmd() *cobra.Command { - var ( - keyToAdd string - valuesToAdd []string - ) - - cmd := &cobra.Command{ - Use: "add", - Short: "Add context to send with alerts. You must specify the output key with the expr value you want", - Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip -cscli lapi context add --key file_source --value evt.Line.Src -cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user - `, - DisableAutoGenTag: true, - RunE: func(_ *cobra.Command, _ []string) error { - hub, err := require.Hub(cli.cfg(), nil, nil) - if err != nil { - return err - } - - if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil { - return fmt.Errorf("while loading context: %w", err) - } - - if keyToAdd != "" { - return cli.addContext(keyToAdd, valuesToAdd) - } - - for _, v := range valuesToAdd { - keySlice := strings.Split(v, ".") - key := keySlice[len(keySlice)-1] - value := []string{v} - if err := cli.addContext(key, value); err != nil { - return err - } - } - - return nil - }, - } - - flags := cmd.Flags() - flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send") - flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key") - - _ = cmd.MarkFlagRequired("value") - - return cmd -} - -func (cli *cliLapi) newContextStatusCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "status", - Short: "List context to send with alerts", - DisableAutoGenTag: true, - RunE: func(_ *cobra.Command, _ []string) error { - cfg := cli.cfg() - hub, err := require.Hub(cfg, nil, nil) - if err != nil { - return err - } - - if err = alertcontext.LoadConsoleContext(cfg, hub); err != nil { - return fmt.Errorf("while loading context: %w", err) - } - - if len(cfg.Crowdsec.ContextToSend) == 0 { - fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.") - return nil - } - - dump, err := yaml.Marshal(cfg.Crowdsec.ContextToSend) - if err != nil { - return fmt.Errorf("unable to show context status: %w", err) - } - - fmt.Print(string(dump)) - - return nil - }, - } - - return cmd -} - -func (cli *cliLapi) newContextDetectCmd() *cobra.Command { - var detectAll bool - - cmd := &cobra.Command{ - Use: "detect", - Short: "Detect available fields from the installed parsers", - Example: `cscli lapi context detect --all -cscli lapi context detect crowdsecurity/sshd-logs - `, - DisableAutoGenTag: true, - RunE: func(cmd *cobra.Command, args []string) error { - cfg := cli.cfg() - if !detectAll && len(args) == 0 { - _ = cmd.Help() - return errors.New("please provide parsers to detect or --all flag") - } - - // to avoid all the log.Info from the loaders functions - log.SetLevel(log.WarnLevel) - - if err := exprhelpers.Init(nil); err != nil { - return fmt.Errorf("failed to init expr helpers: %w", err) - } - - hub, err := require.Hub(cfg, nil, nil) - if err != nil { - return err - } - - csParsers := parser.NewParsers(hub) - if csParsers, err = parser.LoadParsers(cfg, csParsers); err != nil { - return fmt.Errorf("unable to load parsers: %w", err) - } - - fieldByParsers := make(map[string][]string) - for _, node := range csParsers.Nodes { - if !detectAll && !slices.Contains(args, node.Name) { - continue - } - if !detectAll { - args = removeFromSlice(node.Name, args) - } - fieldByParsers[node.Name] = make([]string, 0) - fieldByParsers[node.Name] = detectNode(node, *csParsers.Ctx) - - subNodeFields := detectSubNode(node, *csParsers.Ctx) - for _, field := range subNodeFields { - if !slices.Contains(fieldByParsers[node.Name], field) { - fieldByParsers[node.Name] = append(fieldByParsers[node.Name], field) - } - } - } - - fmt.Printf("Acquisition :\n\n") - fmt.Printf(" - evt.Line.Module\n") - fmt.Printf(" - evt.Line.Raw\n") - fmt.Printf(" - evt.Line.Src\n") - fmt.Println() - - parsersKey := make([]string, 0) - for k := range fieldByParsers { - parsersKey = append(parsersKey, k) - } - sort.Strings(parsersKey) - - for _, k := range parsersKey { - if len(fieldByParsers[k]) == 0 { - continue - } - fmt.Printf("%s :\n\n", k) - values := fieldByParsers[k] - sort.Strings(values) - for _, value := range values { - fmt.Printf(" - %s\n", value) - } - fmt.Println() - } - - if len(args) > 0 { - for _, parserNotFound := range args { - log.Errorf("parser '%s' not found, can't detect fields", parserNotFound) - } - } - - return nil - }, - } - cmd.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser") - - return cmd -} - -func (cli *cliLapi) newContextDeleteCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "delete", - DisableAutoGenTag: true, - RunE: func(_ *cobra.Command, _ []string) error { - filePath := cli.cfg().Crowdsec.ConsoleContextPath - if filePath == "" { - filePath = "the context file" - } - - return fmt.Errorf("command 'delete' has been removed, please manually edit %s", filePath) - }, - } - - return cmd -} - -func (cli *cliLapi) newContextCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "context [command]", - Short: "Manage context to send with alerts", - DisableAutoGenTag: true, - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { - cfg := cli.cfg() - if err := cfg.LoadCrowdsec(); err != nil { - fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", cfg.Crowdsec.ConsoleContextPath) - if err.Error() != fileNotFoundMessage { - return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err) - } - } - if cfg.DisableAgent { - return errors.New("agent is disabled and lapi context can only be used on the agent") - } - - return nil - }, - } - - cmd.AddCommand(cli.newContextAddCmd()) - cmd.AddCommand(cli.newContextStatusCmd()) - cmd.AddCommand(cli.newContextDetectCmd()) - cmd.AddCommand(cli.newContextDeleteCmd()) - - return cmd -} - -func detectStaticField(grokStatics []parser.ExtraField) []string { - ret := make([]string, 0) - - for _, static := range grokStatics { - if static.Parsed != "" { - fieldName := "evt.Parsed." + static.Parsed - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - - if static.Meta != "" { - fieldName := "evt.Meta." + static.Meta - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - - if static.TargetByName != "" { - fieldName := static.TargetByName - if !strings.HasPrefix(fieldName, "evt.") { - fieldName = "evt." + fieldName - } - - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - } - - return ret -} - -func detectNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { - ret := make([]string, 0) - - if node.Grok.RunTimeRegexp != nil { - for _, capturedField := range node.Grok.RunTimeRegexp.Names() { - fieldName := "evt.Parsed." + capturedField - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - } - - if node.Grok.RegexpName != "" { - grokCompiled, err := parserCTX.Grok.Get(node.Grok.RegexpName) - // ignore error (parser does not exist?) - if err == nil { - for _, capturedField := range grokCompiled.Names() { - fieldName := "evt.Parsed." + capturedField - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - } - } - - if len(node.Grok.Statics) > 0 { - staticsField := detectStaticField(node.Grok.Statics) - for _, staticField := range staticsField { - if !slices.Contains(ret, staticField) { - ret = append(ret, staticField) - } - } - } - - if len(node.Statics) > 0 { - staticsField := detectStaticField(node.Statics) - for _, staticField := range staticsField { - if !slices.Contains(ret, staticField) { - ret = append(ret, staticField) - } - } - } - - return ret -} - -func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { - ret := make([]string, 0) - - for _, subnode := range node.LeavesNodes { - if subnode.Grok.RunTimeRegexp != nil { - for _, capturedField := range subnode.Grok.RunTimeRegexp.Names() { - fieldName := "evt.Parsed." + capturedField - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - } - - if subnode.Grok.RegexpName != "" { - grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName) - if err == nil { - // ignore error (parser does not exist?) - for _, capturedField := range grokCompiled.Names() { - fieldName := "evt.Parsed." + capturedField - if !slices.Contains(ret, fieldName) { - ret = append(ret, fieldName) - } - } - } - } - - if len(subnode.Grok.Statics) > 0 { - staticsField := detectStaticField(subnode.Grok.Statics) - for _, staticField := range staticsField { - if !slices.Contains(ret, staticField) { - ret = append(ret, staticField) - } - } - } - - if len(subnode.Statics) > 0 { - staticsField := detectStaticField(subnode.Statics) - for _, staticField := range staticsField { - if !slices.Contains(ret, staticField) { - ret = append(ret, staticField) - } - } - } - } - - return ret -} diff --git a/cmd/crowdsec-cli/clilapi/register.go b/cmd/crowdsec-cli/clilapi/register.go new file mode 100644 index 00000000000..4c9b0f39903 --- /dev/null +++ b/cmd/crowdsec-cli/clilapi/register.go @@ -0,0 +1,117 @@ +package clilapi + +import ( + "context" + "fmt" + "os" + + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen" + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" +) + +func (cli *cliLapi) register(ctx context.Context, apiURL string, outputFile string, machine string, token string) error { + var err error + + lapiUser := machine + cfg := cli.cfg() + + if lapiUser == "" { + lapiUser, err = idgen.GenerateMachineID("") + if err != nil { + return fmt.Errorf("unable to generate machine id: %w", err) + } + } + + password := strfmt.Password(idgen.GeneratePassword(idgen.PasswordLength)) + + apiurl, err := prepareAPIURL(cfg.API.Client, apiURL) + if err != nil { + return fmt.Errorf("parsing api url: %w", err) + } + + _, err = apiclient.RegisterClient(ctx, &apiclient.Config{ + MachineID: lapiUser, + Password: password, + RegistrationToken: token, + URL: apiurl, + VersionPrefix: LAPIURLPrefix, + }, nil) + if err != nil { + return fmt.Errorf("api client register: %w", err) + } + + log.Printf("Successfully registered to Local API (LAPI)") + + var dumpFile string + + if outputFile != "" { + dumpFile = outputFile + } else if cfg.API.Client.CredentialsFilePath != "" { + dumpFile = cfg.API.Client.CredentialsFilePath + } else { + dumpFile = "" + } + + apiCfg := cfg.API.Client.Credentials + apiCfg.Login = lapiUser + apiCfg.Password = password.String() + + if apiURL != "" { + apiCfg.URL = apiURL + } + + apiConfigDump, err := yaml.Marshal(apiCfg) + if err != nil { + return fmt.Errorf("unable to serialize api credentials: %w", err) + } + + if dumpFile != "" { + err = os.WriteFile(dumpFile, apiConfigDump, 0o600) + if err != nil { + return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err) + } + + log.Printf("Local API credentials written to '%s'", dumpFile) + } else { + fmt.Printf("%s\n", string(apiConfigDump)) + } + + log.Warning(reload.Message) + + return nil +} + +func (cli *cliLapi) newRegisterCmd() *cobra.Command { + var ( + apiURL string + outputFile string + machine string + token string + ) + + cmd := &cobra.Command{ + Use: "register", + Short: "Register a machine to Local API (LAPI)", + Long: `Register your machine to the Local API (LAPI). +Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`, + Args: cobra.MinimumNArgs(0), + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.register(cmd.Context(), apiURL, outputFile, machine, token) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)") + flags.StringVarP(&outputFile, "file", "f", "", "output file destination") + flags.StringVar(&machine, "machine", "", "Name of the machine to register with") + flags.StringVar(&token, "token", "", "Auto registration token to use") + + return cmd +} diff --git a/cmd/crowdsec-cli/clilapi/status.go b/cmd/crowdsec-cli/clilapi/status.go new file mode 100644 index 00000000000..6ff88834602 --- /dev/null +++ b/cmd/crowdsec-cli/clilapi/status.go @@ -0,0 +1,115 @@ +package clilapi + +import ( + "context" + "errors" + "fmt" + "io" + "net/url" + "strings" + + "github.com/fatih/color" + "github.com/go-openapi/strfmt" + "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/cwhub" + "github.com/crowdsecurity/crowdsec/pkg/models" +) + +const LAPIURLPrefix = "v1" + +// queryLAPIStatus checks if the Local API is reachable, and if the credentials are correct. +func queryLAPIStatus(ctx context.Context, hub *cwhub.Hub, credURL string, login string, password string) (bool, error) { + apiURL, err := url.Parse(credURL) + if err != nil { + return false, err + } + + client, err := apiclient.NewDefaultClient(apiURL, + LAPIURLPrefix, + "", + nil) + if err != nil { + return false, err + } + + pw := strfmt.Password(password) + + itemsForAPI := hub.GetInstalledListForAPI() + + t := models.WatcherAuthRequest{ + MachineID: &login, + Password: &pw, + Scenarios: itemsForAPI, + } + + _, _, err = client.Auth.AuthenticateWatcher(ctx, t) + if err != nil { + return false, err + } + + return true, nil +} + +func (cli *cliLapi) Status(ctx context.Context, out io.Writer, hub *cwhub.Hub) error { + cfg := cli.cfg() + + cred := cfg.API.Client.Credentials + + fmt.Fprintf(out, "Loaded credentials from %s\n", cfg.API.Client.CredentialsFilePath) + fmt.Fprintf(out, "Trying to authenticate with username %s on %s\n", cred.Login, cred.URL) + + _, err := queryLAPIStatus(ctx, hub, cred.URL, cred.Login, cred.Password) + if err != nil { + return fmt.Errorf("failed to authenticate to Local API (LAPI): %w", err) + } + + fmt.Fprintf(out, "You can successfully interact with Local API (LAPI)\n") + + return nil +} + +// prepareAPIURL checks/fixes a LAPI connection url (http, https or socket) and returns an URL struct +func prepareAPIURL(clientCfg *csconfig.LocalApiClientCfg, apiURL string) (*url.URL, error) { + if apiURL == "" { + if clientCfg == nil || clientCfg.Credentials == nil || clientCfg.Credentials.URL == "" { + return nil, errors.New("no Local API URL. Please provide it in your configuration or with the -u parameter") + } + + apiURL = clientCfg.Credentials.URL + } + + // URL needs to end with /, but user doesn't care + if !strings.HasSuffix(apiURL, "/") { + apiURL += "/" + } + + // URL needs to start with http://, but user doesn't care + if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") && !strings.HasPrefix(apiURL, "/") { + apiURL = "http://" + apiURL + } + + return url.Parse(apiURL) +} + +func (cli *cliLapi) newStatusCmd() *cobra.Command { + cmdLapiStatus := &cobra.Command{ + Use: "status", + Short: "Check authentication to Local API (LAPI)", + Args: cobra.MinimumNArgs(0), + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + hub, err := require.Hub(cli.cfg(), nil, nil) + if err != nil { + return err + } + + return cli.Status(cmd.Context(), color.Output, hub) + }, + } + + return cmdLapiStatus +} diff --git a/cmd/crowdsec-cli/clilapi/lapi_test.go b/cmd/crowdsec-cli/clilapi/status_test.go similarity index 100% rename from cmd/crowdsec-cli/clilapi/lapi_test.go rename to cmd/crowdsec-cli/clilapi/status_test.go From 59a5098239d24077641209883d9a6b925cbf5f3a Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 30 Oct 2024 10:41:28 +0100 Subject: [PATCH 6/6] lint --- cmd/crowdsec-cli/clialert/alerts.go | 2 +- cmd/crowdsec-cli/clidecision/table.go | 3 +-- cmd/crowdsec-cli/cliexplain/explain.go | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/crowdsec-cli/clialert/alerts.go b/cmd/crowdsec-cli/clialert/alerts.go index 425b9860fc9..5907d4a0fa8 100644 --- a/cmd/crowdsec-cli/clialert/alerts.go +++ b/cmd/crowdsec-cli/clialert/alerts.go @@ -36,7 +36,7 @@ func decisionsFromAlert(alert *models.Alert) string { for _, decision := range alert.Decisions { k := *decision.Type if *decision.Simulated { - k = fmt.Sprintf("(simul)%s", k) + k = "(simul)" + k } v := decMap[k] diff --git a/cmd/crowdsec-cli/clidecision/table.go b/cmd/crowdsec-cli/clidecision/table.go index 90a0ae1176b..189eb80b8e5 100644 --- a/cmd/crowdsec-cli/clidecision/table.go +++ b/cmd/crowdsec-cli/clidecision/table.go @@ -1,7 +1,6 @@ package clidecision import ( - "fmt" "io" "strconv" @@ -23,7 +22,7 @@ func (cli *cliDecisions) decisionsTable(out io.Writer, alerts *models.GetAlertsR for _, alertItem := range *alerts { for _, decisionItem := range alertItem.Decisions { if *alertItem.Simulated { - *decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type) + *decisionItem.Type = "(simul)" + *decisionItem.Type } row := []string{ diff --git a/cmd/crowdsec-cli/cliexplain/explain.go b/cmd/crowdsec-cli/cliexplain/explain.go index c7337a86024..d6e821e4e6c 100644 --- a/cmd/crowdsec-cli/cliexplain/explain.go +++ b/cmd/crowdsec-cli/cliexplain/explain.go @@ -197,7 +197,7 @@ func (cli *cliExplain) run() error { return fmt.Errorf("unable to get absolute path of '%s', exiting", logFile) } - dsn = fmt.Sprintf("file://%s", absolutePath) + dsn = "file://" + absolutePath lineCount, err := getLineCountForFile(absolutePath) if err != nil {