123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- package main
- import (
- "encoding/csv"
- "encoding/json"
- "fmt"
- "os"
- "slices"
- "strings"
- "time"
- "github.com/AlecAivazis/survey/v2"
- "github.com/fatih/color"
- log "github.com/sirupsen/logrus"
- "github.com/spf13/cobra"
- "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
- middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
- "github.com/crowdsecurity/crowdsec/pkg/database"
- "github.com/crowdsecurity/crowdsec/pkg/types"
- )
- func askYesNo(message string, defaultAnswer bool) (bool, error) {
- var answer bool
- prompt := &survey.Confirm{
- Message: message,
- Default: defaultAnswer,
- }
- if err := survey.AskOne(prompt, &answer); err != nil {
- return defaultAnswer, err
- }
- return answer, nil
- }
- type cliBouncers struct {
- db *database.Client
- cfg configGetter
- }
- func NewCLIBouncers(cfg configGetter) *cliBouncers {
- return &cliBouncers{
- cfg: cfg,
- }
- }
- func (cli *cliBouncers) NewCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "bouncers [action]",
- Short: "Manage bouncers [requires local API]",
- Long: `To list/add/delete/prune bouncers.
- Note: This command requires database direct access, so is intended to be run on Local API/master.
- `,
- Args: cobra.MinimumNArgs(1),
- Aliases: []string{"bouncer"},
- DisableAutoGenTag: true,
- PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
- var err error
- if err = require.LAPI(cli.cfg()); err != nil {
- return err
- }
- cli.db, err = database.NewClient(cli.cfg().DbConfig)
- if err != nil {
- return fmt.Errorf("can't connect to the database: %s", err)
- }
- return nil
- },
- }
- cmd.AddCommand(cli.newListCmd())
- cmd.AddCommand(cli.newAddCmd())
- cmd.AddCommand(cli.newDeleteCmd())
- cmd.AddCommand(cli.newPruneCmd())
- return cmd
- }
- func (cli *cliBouncers) list() error {
- out := color.Output
- bouncers, err := cli.db.ListBouncers()
- if err != nil {
- return fmt.Errorf("unable to list bouncers: %s", err)
- }
- switch cli.cfg().Cscli.Output {
- case "human":
- getBouncersTable(out, bouncers)
- case "json":
- enc := json.NewEncoder(out)
- enc.SetIndent("", " ")
- if err := enc.Encode(bouncers); err != nil {
- return fmt.Errorf("failed to marshal: %w", err)
- }
- return nil
- case "raw":
- csvwriter := csv.NewWriter(out)
- if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
- return fmt.Errorf("failed to write raw header: %w", err)
- }
- for _, b := range bouncers {
- valid := "validated"
- if b.Revoked {
- valid = "pending"
- }
- if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}); err != nil {
- return fmt.Errorf("failed to write raw: %w", err)
- }
- }
- csvwriter.Flush()
- }
- return nil
- }
- func (cli *cliBouncers) newListCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "list",
- Short: "list all bouncers within the database",
- Example: `cscli bouncers list`,
- Args: cobra.ExactArgs(0),
- DisableAutoGenTag: true,
- RunE: func(_ *cobra.Command, _ []string) error {
- return cli.list()
- },
- }
- return cmd
- }
- func (cli *cliBouncers) add(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: %s", err)
- }
- }
- _, err = cli.db.CreateBouncer(bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
- if err != nil {
- return fmt.Errorf("unable to create bouncer: %s", 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 fmt.Errorf("unable to marshal 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 <random-key>`,
- Args: cobra.ExactArgs(1),
- DisableAutoGenTag: true,
- RunE: func(_ *cobra.Command, args []string) error {
- return cli.add(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
- }
- func (cli *cliBouncers) deleteValid(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- bouncers, err := cli.db.ListBouncers()
- if err != nil {
- cobra.CompError("unable to list bouncers " + err.Error())
- }
- ret := []string{}
- for _, bouncer := range bouncers {
- if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
- ret = append(ret, bouncer.Name)
- }
- }
- return ret, cobra.ShellCompDirectiveNoFileComp
- }
- func (cli *cliBouncers) delete(bouncers []string) error {
- for _, bouncerID := range bouncers {
- err := cli.db.DeleteBouncer(bouncerID)
- if err != nil {
- return fmt.Errorf("unable to delete bouncer '%s': %s", bouncerID, err)
- }
- log.Infof("bouncer '%s' deleted successfully", bouncerID)
- }
- return nil
- }
- func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
- cmd := &cobra.Command{
- Use: "delete MyBouncerName",
- Short: "delete bouncer(s) from the database",
- Args: cobra.MinimumNArgs(1),
- Aliases: []string{"remove"},
- DisableAutoGenTag: true,
- ValidArgsFunction: cli.deleteValid,
- RunE: func(_ *cobra.Command, args []string) error {
- return cli.delete(args)
- },
- }
- return cmd
- }
- func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
- if duration < 2*time.Minute {
- if yes, err := askYesNo(
- "The duration you provided is less than 2 minutes. " +
- "This may remove active bouncers. Continue?", false); err != nil {
- return err
- } else if !yes {
- fmt.Println("User aborted prune. No changes were made.")
- return nil
- }
- }
- bouncers, err := cli.db.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(duration))
- if err != nil {
- return fmt.Errorf("unable to query bouncers: %w", err)
- }
- if len(bouncers) == 0 {
- fmt.Println("No bouncers to prune.")
- return nil
- }
- getBouncersTable(color.Output, bouncers)
- if !force {
- if yes, err := askYesNo(
- "You are about to PERMANENTLY remove the above bouncers from the database. " +
- "These will NOT be recoverable. Continue?", false); err != nil {
- return err
- } else if !yes {
- fmt.Println("User aborted prune. No changes were made.")
- return nil
- }
- }
- deleted, err := cli.db.BulkDeleteBouncers(bouncers)
- if err != nil {
- return fmt.Errorf("unable to prune bouncers: %s", err)
- }
- fmt.Fprintf(os.Stderr, "Successfully deleted %d bouncers\n", deleted)
- return nil
- }
- func (cli *cliBouncers) newPruneCmd() *cobra.Command {
- var (
- duration time.Duration
- force bool
- )
- const defaultDuration = 60 * time.Minute
- cmd := &cobra.Command{
- 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(_ *cobra.Command, _ []string) error {
- return cli.prune(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
- }
|