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/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 226fbb7e922..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.ExactArgs(0), - 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 +} 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/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 +} 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