github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/bouncers.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/csv"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"slices"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/AlecAivazis/survey/v2"
    14  	"github.com/fatih/color"
    15  	log "github.com/sirupsen/logrus"
    16  	"github.com/spf13/cobra"
    17  
    18  	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
    19  	middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
    20  	"github.com/crowdsecurity/crowdsec/pkg/database"
    21  	"github.com/crowdsecurity/crowdsec/pkg/types"
    22  )
    23  
    24  func askYesNo(message string, defaultAnswer bool) (bool, error) {
    25  	var answer bool
    26  
    27  	prompt := &survey.Confirm{
    28  		Message: message,
    29  		Default: defaultAnswer,
    30  	}
    31  
    32  	if err := survey.AskOne(prompt, &answer); err != nil {
    33  		return defaultAnswer, err
    34  	}
    35  
    36  	return answer, nil
    37  }
    38  
    39  type cliBouncers struct {
    40  	db  *database.Client
    41  	cfg configGetter
    42  }
    43  
    44  func NewCLIBouncers(cfg configGetter) *cliBouncers {
    45  	return &cliBouncers{
    46  		cfg: cfg,
    47  	}
    48  }
    49  
    50  func (cli *cliBouncers) NewCommand() *cobra.Command {
    51  	cmd := &cobra.Command{
    52  		Use:   "bouncers [action]",
    53  		Short: "Manage bouncers [requires local API]",
    54  		Long: `To list/add/delete/prune bouncers.
    55  Note: This command requires database direct access, so is intended to be run on Local API/master.
    56  `,
    57  		Args:              cobra.MinimumNArgs(1),
    58  		Aliases:           []string{"bouncer"},
    59  		DisableAutoGenTag: true,
    60  		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
    61  			var err error
    62  
    63  			cfg := cli.cfg()
    64  
    65  			if err = require.LAPI(cfg); err != nil {
    66  				return err
    67  			}
    68  
    69  			cli.db, err = database.NewClient(cfg.DbConfig)
    70  			if err != nil {
    71  				return fmt.Errorf("can't connect to the database: %w", err)
    72  			}
    73  
    74  			return nil
    75  		},
    76  	}
    77  
    78  	cmd.AddCommand(cli.newListCmd())
    79  	cmd.AddCommand(cli.newAddCmd())
    80  	cmd.AddCommand(cli.newDeleteCmd())
    81  	cmd.AddCommand(cli.newPruneCmd())
    82  
    83  	return cmd
    84  }
    85  
    86  func (cli *cliBouncers) list() error {
    87  	out := color.Output
    88  
    89  	bouncers, err := cli.db.ListBouncers()
    90  	if err != nil {
    91  		return fmt.Errorf("unable to list bouncers: %w", err)
    92  	}
    93  
    94  	switch cli.cfg().Cscli.Output {
    95  	case "human":
    96  		getBouncersTable(out, bouncers)
    97  	case "json":
    98  		enc := json.NewEncoder(out)
    99  		enc.SetIndent("", "  ")
   100  
   101  		if err := enc.Encode(bouncers); err != nil {
   102  			return fmt.Errorf("failed to marshal: %w", err)
   103  		}
   104  
   105  		return nil
   106  	case "raw":
   107  		csvwriter := csv.NewWriter(out)
   108  
   109  		if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
   110  			return fmt.Errorf("failed to write raw header: %w", err)
   111  		}
   112  
   113  		for _, b := range bouncers {
   114  			valid := "validated"
   115  			if b.Revoked {
   116  				valid = "pending"
   117  			}
   118  
   119  			if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}); err != nil {
   120  				return fmt.Errorf("failed to write raw: %w", err)
   121  			}
   122  		}
   123  
   124  		csvwriter.Flush()
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  func (cli *cliBouncers) newListCmd() *cobra.Command {
   131  	cmd := &cobra.Command{
   132  		Use:               "list",
   133  		Short:             "list all bouncers within the database",
   134  		Example:           `cscli bouncers list`,
   135  		Args:              cobra.ExactArgs(0),
   136  		DisableAutoGenTag: true,
   137  		RunE: func(_ *cobra.Command, _ []string) error {
   138  			return cli.list()
   139  		},
   140  	}
   141  
   142  	return cmd
   143  }
   144  
   145  func (cli *cliBouncers) add(bouncerName string, key string) error {
   146  	var err error
   147  
   148  	keyLength := 32
   149  
   150  	if key == "" {
   151  		key, err = middlewares.GenerateAPIKey(keyLength)
   152  		if err != nil {
   153  			return fmt.Errorf("unable to generate api key: %w", err)
   154  		}
   155  	}
   156  
   157  	_, err = cli.db.CreateBouncer(bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
   158  	if err != nil {
   159  		return fmt.Errorf("unable to create bouncer: %w", err)
   160  	}
   161  
   162  	switch cli.cfg().Cscli.Output {
   163  	case "human":
   164  		fmt.Printf("API key for '%s':\n\n", bouncerName)
   165  		fmt.Printf("   %s\n\n", key)
   166  		fmt.Print("Please keep this key since you will not be able to retrieve it!\n")
   167  	case "raw":
   168  		fmt.Print(key)
   169  	case "json":
   170  		j, err := json.Marshal(key)
   171  		if err != nil {
   172  			return errors.New("unable to marshal api key")
   173  		}
   174  
   175  		fmt.Print(string(j))
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func (cli *cliBouncers) newAddCmd() *cobra.Command {
   182  	var key string
   183  
   184  	cmd := &cobra.Command{
   185  		Use:   "add MyBouncerName",
   186  		Short: "add a single bouncer to the database",
   187  		Example: `cscli bouncers add MyBouncerName
   188  cscli bouncers add MyBouncerName --key <random-key>`,
   189  		Args:              cobra.ExactArgs(1),
   190  		DisableAutoGenTag: true,
   191  		RunE: func(_ *cobra.Command, args []string) error {
   192  			return cli.add(args[0], key)
   193  		},
   194  	}
   195  
   196  	flags := cmd.Flags()
   197  	flags.StringP("length", "l", "", "length of the api key")
   198  	_ = flags.MarkDeprecated("length", "use --key instead")
   199  	flags.StringVarP(&key, "key", "k", "", "api key for the bouncer")
   200  
   201  	return cmd
   202  }
   203  
   204  func (cli *cliBouncers) deleteValid(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   205  	bouncers, err := cli.db.ListBouncers()
   206  	if err != nil {
   207  		cobra.CompError("unable to list bouncers " + err.Error())
   208  	}
   209  
   210  	ret := []string{}
   211  
   212  	for _, bouncer := range bouncers {
   213  		if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
   214  			ret = append(ret, bouncer.Name)
   215  		}
   216  	}
   217  
   218  	return ret, cobra.ShellCompDirectiveNoFileComp
   219  }
   220  
   221  func (cli *cliBouncers) delete(bouncers []string) error {
   222  	for _, bouncerID := range bouncers {
   223  		err := cli.db.DeleteBouncer(bouncerID)
   224  		if err != nil {
   225  			return fmt.Errorf("unable to delete bouncer '%s': %w", bouncerID, err)
   226  		}
   227  
   228  		log.Infof("bouncer '%s' deleted successfully", bouncerID)
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
   235  	cmd := &cobra.Command{
   236  		Use:               "delete MyBouncerName",
   237  		Short:             "delete bouncer(s) from the database",
   238  		Args:              cobra.MinimumNArgs(1),
   239  		Aliases:           []string{"remove"},
   240  		DisableAutoGenTag: true,
   241  		ValidArgsFunction: cli.deleteValid,
   242  		RunE: func(_ *cobra.Command, args []string) error {
   243  			return cli.delete(args)
   244  		},
   245  	}
   246  
   247  	return cmd
   248  }
   249  
   250  func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
   251  	if duration < 2*time.Minute {
   252  		if yes, err := askYesNo(
   253  				"The duration you provided is less than 2 minutes. " +
   254  				"This may remove active bouncers. Continue?", false); err != nil {
   255  			return err
   256  		} else if !yes {
   257  			fmt.Println("User aborted prune. No changes were made.")
   258  			return nil
   259  		}
   260  	}
   261  
   262  	bouncers, err := cli.db.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(-duration))
   263  	if err != nil {
   264  		return fmt.Errorf("unable to query bouncers: %w", err)
   265  	}
   266  
   267  	if len(bouncers) == 0 {
   268  		fmt.Println("No bouncers to prune.")
   269  		return nil
   270  	}
   271  
   272  	getBouncersTable(color.Output, bouncers)
   273  
   274  	if !force {
   275  		if yes, err := askYesNo(
   276  				"You are about to PERMANENTLY remove the above bouncers from the database. " +
   277  				"These will NOT be recoverable. Continue?", false); err != nil {
   278  			return err
   279  		} else if !yes {
   280  			fmt.Println("User aborted prune. No changes were made.")
   281  			return nil
   282  		}
   283  	}
   284  
   285  	deleted, err := cli.db.BulkDeleteBouncers(bouncers)
   286  	if err != nil {
   287  		return fmt.Errorf("unable to prune bouncers: %w", err)
   288  	}
   289  
   290  	fmt.Fprintf(os.Stderr, "Successfully deleted %d bouncers\n", deleted)
   291  
   292  	return nil
   293  }
   294  
   295  func (cli *cliBouncers) newPruneCmd() *cobra.Command {
   296  	var (
   297  		duration time.Duration
   298  		force    bool
   299  	)
   300  
   301  	const defaultDuration = 60 * time.Minute
   302  
   303  	cmd := &cobra.Command{
   304  		Use:               "prune",
   305  		Short:             "prune multiple bouncers from the database",
   306  		Args:              cobra.NoArgs,
   307  		DisableAutoGenTag: true,
   308  		Example: `cscli bouncers prune -d 45m
   309  cscli bouncers prune -d 45m --force`,
   310  		RunE: func(_ *cobra.Command, _ []string) error {
   311  			return cli.prune(duration, force)
   312  		},
   313  	}
   314  
   315  	flags := cmd.Flags()
   316  	flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since last pull")
   317  	flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
   318  
   319  	return cmd
   320  }