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

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/url"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/fatih/color"
    15  	"github.com/go-openapi/strfmt"
    16  	log "github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  
    19  	"github.com/crowdsecurity/go-cs-lib/version"
    20  
    21  	"github.com/crowdsecurity/crowdsec/pkg/apiclient"
    22  	"github.com/crowdsecurity/crowdsec/pkg/models"
    23  	"github.com/crowdsecurity/crowdsec/pkg/types"
    24  )
    25  
    26  var Client *apiclient.ApiClient
    27  
    28  func (cli *cliDecisions) decisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
    29  	/*here we cheat a bit : to make it more readable for the user, we dedup some entries*/
    30  	spamLimit := make(map[string]bool)
    31  	skipped := 0
    32  
    33  	for aIdx := 0; aIdx < len(*alerts); aIdx++ {
    34  		alertItem := (*alerts)[aIdx]
    35  		newDecisions := make([]*models.Decision, 0)
    36  
    37  		for _, decisionItem := range alertItem.Decisions {
    38  			spamKey := fmt.Sprintf("%t:%s:%s:%s", *decisionItem.Simulated, *decisionItem.Type, *decisionItem.Scope, *decisionItem.Value)
    39  			if _, ok := spamLimit[spamKey]; ok {
    40  				skipped++
    41  				continue
    42  			}
    43  
    44  			spamLimit[spamKey] = true
    45  
    46  			newDecisions = append(newDecisions, decisionItem)
    47  		}
    48  
    49  		alertItem.Decisions = newDecisions
    50  	}
    51  
    52  	switch cli.cfg().Cscli.Output {
    53  	case "raw":
    54  		csvwriter := csv.NewWriter(os.Stdout)
    55  		header := []string{"id", "source", "ip", "reason", "action", "country", "as", "events_count", "expiration", "simulated", "alert_id"}
    56  
    57  		if printMachine {
    58  			header = append(header, "machine")
    59  		}
    60  
    61  		err := csvwriter.Write(header)
    62  		if err != nil {
    63  			return err
    64  		}
    65  
    66  		for _, alertItem := range *alerts {
    67  			for _, decisionItem := range alertItem.Decisions {
    68  				raw := []string{
    69  					fmt.Sprintf("%d", decisionItem.ID),
    70  					*decisionItem.Origin,
    71  					*decisionItem.Scope + ":" + *decisionItem.Value,
    72  					*decisionItem.Scenario,
    73  					*decisionItem.Type,
    74  					alertItem.Source.Cn,
    75  					alertItem.Source.GetAsNumberName(),
    76  					fmt.Sprintf("%d", *alertItem.EventsCount),
    77  					*decisionItem.Duration,
    78  					fmt.Sprintf("%t", *decisionItem.Simulated),
    79  					fmt.Sprintf("%d", alertItem.ID),
    80  				}
    81  				if printMachine {
    82  					raw = append(raw, alertItem.MachineID)
    83  				}
    84  
    85  				err := csvwriter.Write(raw)
    86  				if err != nil {
    87  					return err
    88  				}
    89  			}
    90  		}
    91  
    92  		csvwriter.Flush()
    93  	case "json":
    94  		if *alerts == nil {
    95  			// avoid returning "null" in `json"
    96  			// could be cleaner if we used slice of alerts directly
    97  			fmt.Println("[]")
    98  			return nil
    99  		}
   100  
   101  		x, _ := json.MarshalIndent(alerts, "", " ")
   102  		fmt.Printf("%s", string(x))
   103  	case "human":
   104  		if len(*alerts) == 0 {
   105  			fmt.Println("No active decisions")
   106  			return nil
   107  		}
   108  
   109  		cli.decisionsTable(color.Output, alerts, printMachine)
   110  
   111  		if skipped > 0 {
   112  			fmt.Printf("%d duplicated entries skipped\n", skipped)
   113  		}
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  type cliDecisions struct {
   120  	cfg configGetter
   121  }
   122  
   123  func NewCLIDecisions(cfg configGetter) *cliDecisions {
   124  	return &cliDecisions{
   125  		cfg: cfg,
   126  	}
   127  }
   128  
   129  func (cli *cliDecisions) NewCommand() *cobra.Command {
   130  	cmd := &cobra.Command{
   131  		Use:     "decisions [action]",
   132  		Short:   "Manage decisions",
   133  		Long:    `Add/List/Delete/Import decisions from LAPI`,
   134  		Example: `cscli decisions [action] [filter]`,
   135  		Aliases: []string{"decision"},
   136  		/*TBD example*/
   137  		Args:              cobra.MinimumNArgs(1),
   138  		DisableAutoGenTag: true,
   139  		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
   140  			cfg := cli.cfg()
   141  			if err := cfg.LoadAPIClient(); err != nil {
   142  				return fmt.Errorf("loading api client: %w", err)
   143  			}
   144  			password := strfmt.Password(cfg.API.Client.Credentials.Password)
   145  			apiurl, err := url.Parse(cfg.API.Client.Credentials.URL)
   146  			if err != nil {
   147  				return fmt.Errorf("parsing api url %s: %w", cfg.API.Client.Credentials.URL, err)
   148  			}
   149  			Client, err = apiclient.NewClient(&apiclient.Config{
   150  				MachineID:     cfg.API.Client.Credentials.Login,
   151  				Password:      password,
   152  				UserAgent:     fmt.Sprintf("crowdsec/%s", version.String()),
   153  				URL:           apiurl,
   154  				VersionPrefix: "v1",
   155  			})
   156  			if err != nil {
   157  				return fmt.Errorf("creating api client: %w", err)
   158  			}
   159  
   160  			return nil
   161  		},
   162  	}
   163  
   164  	cmd.AddCommand(cli.newListCmd())
   165  	cmd.AddCommand(cli.newAddCmd())
   166  	cmd.AddCommand(cli.newDeleteCmd())
   167  	cmd.AddCommand(cli.newImportCmd())
   168  
   169  	return cmd
   170  }
   171  
   172  func (cli *cliDecisions) newListCmd() *cobra.Command {
   173  	var filter = apiclient.AlertsListOpts{
   174  		ValueEquals:    new(string),
   175  		ScopeEquals:    new(string),
   176  		ScenarioEquals: new(string),
   177  		OriginEquals:   new(string),
   178  		IPEquals:       new(string),
   179  		RangeEquals:    new(string),
   180  		Since:          new(string),
   181  		Until:          new(string),
   182  		TypeEquals:     new(string),
   183  		IncludeCAPI:    new(bool),
   184  		Limit:          new(int),
   185  	}
   186  
   187  	NoSimu := new(bool)
   188  	contained := new(bool)
   189  
   190  	var printMachine bool
   191  
   192  	cmd := &cobra.Command{
   193  		Use:   "list [options]",
   194  		Short: "List decisions from LAPI",
   195  		Example: `cscli decisions list -i 1.2.3.4
   196  cscli decisions list -r 1.2.3.0/24
   197  cscli decisions list -s crowdsecurity/ssh-bf
   198  cscli decisions list --origin lists --scenario list_name
   199  `,
   200  		Args:              cobra.ExactArgs(0),
   201  		DisableAutoGenTag: true,
   202  		RunE: func(cmd *cobra.Command, _ []string) error {
   203  			var err error
   204  			/*take care of shorthand options*/
   205  			if err = manageCliDecisionAlerts(filter.IPEquals, filter.RangeEquals, filter.ScopeEquals, filter.ValueEquals); err != nil {
   206  				return err
   207  			}
   208  			filter.ActiveDecisionEquals = new(bool)
   209  			*filter.ActiveDecisionEquals = true
   210  			if NoSimu != nil && *NoSimu {
   211  				filter.IncludeSimulated = new(bool)
   212  			}
   213  			/* nullify the empty entries to avoid bad filter */
   214  			if *filter.Until == "" {
   215  				filter.Until = nil
   216  			} else if strings.HasSuffix(*filter.Until, "d") {
   217  				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
   218  				realDuration := strings.TrimSuffix(*filter.Until, "d")
   219  				days, err := strconv.Atoi(realDuration)
   220  				if err != nil {
   221  					printHelp(cmd)
   222  					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Until)
   223  				}
   224  				*filter.Until = fmt.Sprintf("%d%s", days*24, "h")
   225  			}
   226  
   227  			if *filter.Since == "" {
   228  				filter.Since = nil
   229  			} else if strings.HasSuffix(*filter.Since, "d") {
   230  				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
   231  				realDuration := strings.TrimSuffix(*filter.Since, "d")
   232  				days, err := strconv.Atoi(realDuration)
   233  				if err != nil {
   234  					printHelp(cmd)
   235  					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Since)
   236  				}
   237  				*filter.Since = fmt.Sprintf("%d%s", days*24, "h")
   238  			}
   239  			if *filter.IncludeCAPI {
   240  				*filter.Limit = 0
   241  			}
   242  			if *filter.TypeEquals == "" {
   243  				filter.TypeEquals = nil
   244  			}
   245  			if *filter.ValueEquals == "" {
   246  				filter.ValueEquals = nil
   247  			}
   248  			if *filter.ScopeEquals == "" {
   249  				filter.ScopeEquals = nil
   250  			}
   251  			if *filter.ScenarioEquals == "" {
   252  				filter.ScenarioEquals = nil
   253  			}
   254  			if *filter.IPEquals == "" {
   255  				filter.IPEquals = nil
   256  			}
   257  			if *filter.RangeEquals == "" {
   258  				filter.RangeEquals = nil
   259  			}
   260  
   261  			if *filter.OriginEquals == "" {
   262  				filter.OriginEquals = nil
   263  			}
   264  
   265  			if contained != nil && *contained {
   266  				filter.Contains = new(bool)
   267  			}
   268  
   269  			alerts, _, err := Client.Alerts.List(context.Background(), filter)
   270  			if err != nil {
   271  				return fmt.Errorf("unable to retrieve decisions: %w", err)
   272  			}
   273  
   274  			err = cli.decisionsToTable(alerts, printMachine)
   275  			if err != nil {
   276  				return fmt.Errorf("unable to print decisions: %w", err)
   277  			}
   278  
   279  			return nil
   280  		},
   281  	}
   282  	cmd.Flags().SortFlags = false
   283  	cmd.Flags().BoolVarP(filter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
   284  	cmd.Flags().StringVar(filter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
   285  	cmd.Flags().StringVar(filter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
   286  	cmd.Flags().StringVarP(filter.TypeEquals, "type", "t", "", "restrict to this decision type (ie. ban,captcha)")
   287  	cmd.Flags().StringVar(filter.ScopeEquals, "scope", "", "restrict to this scope (ie. ip,range,session)")
   288  	cmd.Flags().StringVar(filter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
   289  	cmd.Flags().StringVarP(filter.ValueEquals, "value", "v", "", "restrict to this value (ie. 1.2.3.4,userName)")
   290  	cmd.Flags().StringVarP(filter.ScenarioEquals, "scenario", "s", "", "restrict to this scenario (ie. crowdsecurity/ssh-bf)")
   291  	cmd.Flags().StringVarP(filter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
   292  	cmd.Flags().StringVarP(filter.RangeEquals, "range", "r", "", "restrict to alerts from this source range (shorthand for --scope range --value <RANGE>)")
   293  	cmd.Flags().IntVarP(filter.Limit, "limit", "l", 100, "number of alerts to get (use 0 to remove the limit)")
   294  	cmd.Flags().BoolVar(NoSimu, "no-simu", false, "exclude decisions in simulation mode")
   295  	cmd.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that triggered decisions")
   296  	cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
   297  
   298  	return cmd
   299  }
   300  
   301  func (cli *cliDecisions) newAddCmd() *cobra.Command {
   302  	var (
   303  		addIP       string
   304  		addRange    string
   305  		addDuration string
   306  		addValue    string
   307  		addScope    string
   308  		addReason   string
   309  		addType     string
   310  	)
   311  
   312  	cmd := &cobra.Command{
   313  		Use:   "add [options]",
   314  		Short: "Add decision to LAPI",
   315  		Example: `cscli decisions add --ip 1.2.3.4
   316  cscli decisions add --range 1.2.3.0/24
   317  cscli decisions add --ip 1.2.3.4 --duration 24h --type captcha
   318  cscli decisions add --scope username --value foobar
   319  `,
   320  		/*TBD : fix long and example*/
   321  		Args:              cobra.ExactArgs(0),
   322  		DisableAutoGenTag: true,
   323  		RunE: func(cmd *cobra.Command, _ []string) error {
   324  			var err error
   325  			alerts := models.AddAlertsRequest{}
   326  			origin := types.CscliOrigin
   327  			capacity := int32(0)
   328  			leakSpeed := "0"
   329  			eventsCount := int32(1)
   330  			empty := ""
   331  			simulated := false
   332  			startAt := time.Now().UTC().Format(time.RFC3339)
   333  			stopAt := time.Now().UTC().Format(time.RFC3339)
   334  			createdAt := time.Now().UTC().Format(time.RFC3339)
   335  
   336  			/*take care of shorthand options*/
   337  			if err = manageCliDecisionAlerts(&addIP, &addRange, &addScope, &addValue); err != nil {
   338  				return err
   339  			}
   340  
   341  			if addIP != "" {
   342  				addValue = addIP
   343  				addScope = types.Ip
   344  			} else if addRange != "" {
   345  				addValue = addRange
   346  				addScope = types.Range
   347  			} else if addValue == "" {
   348  				printHelp(cmd)
   349  				return fmt.Errorf("missing arguments, a value is required (--ip, --range or --scope and --value)")
   350  			}
   351  
   352  			if addReason == "" {
   353  				addReason = fmt.Sprintf("manual '%s' from '%s'", addType, cli.cfg().API.Client.Credentials.Login)
   354  			}
   355  			decision := models.Decision{
   356  				Duration: &addDuration,
   357  				Scope:    &addScope,
   358  				Value:    &addValue,
   359  				Type:     &addType,
   360  				Scenario: &addReason,
   361  				Origin:   &origin,
   362  			}
   363  			alert := models.Alert{
   364  				Capacity:        &capacity,
   365  				Decisions:       []*models.Decision{&decision},
   366  				Events:          []*models.Event{},
   367  				EventsCount:     &eventsCount,
   368  				Leakspeed:       &leakSpeed,
   369  				Message:         &addReason,
   370  				ScenarioHash:    &empty,
   371  				Scenario:        &addReason,
   372  				ScenarioVersion: &empty,
   373  				Simulated:       &simulated,
   374  				//setting empty scope/value broke plugins, and it didn't seem to be needed anymore w/ latest papi changes
   375  				Source: &models.Source{
   376  					AsName:   empty,
   377  					AsNumber: empty,
   378  					Cn:       empty,
   379  					IP:       addValue,
   380  					Range:    "",
   381  					Scope:    &addScope,
   382  					Value:    &addValue,
   383  				},
   384  				StartAt:   &startAt,
   385  				StopAt:    &stopAt,
   386  				CreatedAt: createdAt,
   387  			}
   388  			alerts = append(alerts, &alert)
   389  
   390  			_, _, err = Client.Alerts.Add(context.Background(), alerts)
   391  			if err != nil {
   392  				return err
   393  			}
   394  
   395  			log.Info("Decision successfully added")
   396  
   397  			return nil
   398  		},
   399  	}
   400  
   401  	cmd.Flags().SortFlags = false
   402  	cmd.Flags().StringVarP(&addIP, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
   403  	cmd.Flags().StringVarP(&addRange, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
   404  	cmd.Flags().StringVarP(&addDuration, "duration", "d", "4h", "Decision duration (ie. 1h,4h,30m)")
   405  	cmd.Flags().StringVarP(&addValue, "value", "v", "", "The value (ie. --scope username --value foobar)")
   406  	cmd.Flags().StringVar(&addScope, "scope", types.Ip, "Decision scope (ie. ip,range,username)")
   407  	cmd.Flags().StringVarP(&addReason, "reason", "R", "", "Decision reason (ie. scenario-name)")
   408  	cmd.Flags().StringVarP(&addType, "type", "t", "ban", "Decision type (ie. ban,captcha,throttle)")
   409  
   410  	return cmd
   411  }
   412  
   413  func (cli *cliDecisions) newDeleteCmd() *cobra.Command {
   414  	var delFilter = apiclient.DecisionsDeleteOpts{
   415  		ScopeEquals:    new(string),
   416  		ValueEquals:    new(string),
   417  		TypeEquals:     new(string),
   418  		IPEquals:       new(string),
   419  		RangeEquals:    new(string),
   420  		ScenarioEquals: new(string),
   421  		OriginEquals:   new(string),
   422  	}
   423  
   424  	var delDecisionID string
   425  
   426  	var delDecisionAll bool
   427  
   428  	contained := new(bool)
   429  
   430  	cmd := &cobra.Command{
   431  		Use:               "delete [options]",
   432  		Short:             "Delete decisions",
   433  		DisableAutoGenTag: true,
   434  		Aliases:           []string{"remove"},
   435  		Example: `cscli decisions delete -r 1.2.3.0/24
   436  cscli decisions delete -i 1.2.3.4
   437  cscli decisions delete --id 42
   438  cscli decisions delete --type captcha
   439  cscli decisions delete --origin lists  --scenario list_name
   440  `,
   441  		/*TBD : refaire le Long/Example*/
   442  		PreRunE: func(cmd *cobra.Command, _ []string) error {
   443  			if delDecisionAll {
   444  				return nil
   445  			}
   446  			if *delFilter.ScopeEquals == "" && *delFilter.ValueEquals == "" &&
   447  				*delFilter.TypeEquals == "" && *delFilter.IPEquals == "" &&
   448  				*delFilter.RangeEquals == "" && *delFilter.ScenarioEquals == "" &&
   449  				*delFilter.OriginEquals == "" && delDecisionID == "" {
   450  				cmd.Usage()
   451  				return fmt.Errorf("at least one filter or --all must be specified")
   452  			}
   453  
   454  			return nil
   455  		},
   456  		RunE: func(_ *cobra.Command, _ []string) error {
   457  			var err error
   458  			var decisions *models.DeleteDecisionResponse
   459  
   460  			/*take care of shorthand options*/
   461  			if err = manageCliDecisionAlerts(delFilter.IPEquals, delFilter.RangeEquals, delFilter.ScopeEquals, delFilter.ValueEquals); err != nil {
   462  				return err
   463  			}
   464  			if *delFilter.ScopeEquals == "" {
   465  				delFilter.ScopeEquals = nil
   466  			}
   467  			if *delFilter.OriginEquals == "" {
   468  				delFilter.OriginEquals = nil
   469  			}
   470  			if *delFilter.ValueEquals == "" {
   471  				delFilter.ValueEquals = nil
   472  			}
   473  			if *delFilter.ScenarioEquals == "" {
   474  				delFilter.ScenarioEquals = nil
   475  			}
   476  			if *delFilter.TypeEquals == "" {
   477  				delFilter.TypeEquals = nil
   478  			}
   479  			if *delFilter.IPEquals == "" {
   480  				delFilter.IPEquals = nil
   481  			}
   482  			if *delFilter.RangeEquals == "" {
   483  				delFilter.RangeEquals = nil
   484  			}
   485  			if contained != nil && *contained {
   486  				delFilter.Contains = new(bool)
   487  			}
   488  
   489  			if delDecisionID == "" {
   490  				decisions, _, err = Client.Decisions.Delete(context.Background(), delFilter)
   491  				if err != nil {
   492  					return fmt.Errorf("unable to delete decisions: %v", err)
   493  				}
   494  			} else {
   495  				if _, err = strconv.Atoi(delDecisionID); err != nil {
   496  					return fmt.Errorf("id '%s' is not an integer: %v", delDecisionID, err)
   497  				}
   498  				decisions, _, err = Client.Decisions.DeleteOne(context.Background(), delDecisionID)
   499  				if err != nil {
   500  					return fmt.Errorf("unable to delete decision: %v", err)
   501  				}
   502  			}
   503  			log.Infof("%s decision(s) deleted", decisions.NbDeleted)
   504  
   505  			return nil
   506  		},
   507  	}
   508  
   509  	cmd.Flags().SortFlags = false
   510  	cmd.Flags().StringVarP(delFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
   511  	cmd.Flags().StringVarP(delFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
   512  	cmd.Flags().StringVarP(delFilter.TypeEquals, "type", "t", "", "the decision type (ie. ban,captcha)")
   513  	cmd.Flags().StringVarP(delFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
   514  	cmd.Flags().StringVarP(delFilter.ScenarioEquals, "scenario", "s", "", "the scenario name (ie. crowdsecurity/ssh-bf)")
   515  	cmd.Flags().StringVar(delFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
   516  
   517  	cmd.Flags().StringVar(&delDecisionID, "id", "", "decision id")
   518  	cmd.Flags().BoolVar(&delDecisionAll, "all", false, "delete all decisions")
   519  	cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
   520  
   521  	return cmd
   522  }