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 }