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

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/csv"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/url"
     9  	"os"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/fatih/color"
    16  	"github.com/go-openapi/strfmt"
    17  	log "github.com/sirupsen/logrus"
    18  	"github.com/spf13/cobra"
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/crowdsecurity/go-cs-lib/version"
    22  
    23  	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
    24  	"github.com/crowdsecurity/crowdsec/pkg/apiclient"
    25  	"github.com/crowdsecurity/crowdsec/pkg/database"
    26  	"github.com/crowdsecurity/crowdsec/pkg/models"
    27  	"github.com/crowdsecurity/crowdsec/pkg/types"
    28  )
    29  
    30  func DecisionsFromAlert(alert *models.Alert) string {
    31  	ret := ""
    32  	decMap := make(map[string]int)
    33  
    34  	for _, decision := range alert.Decisions {
    35  		k := *decision.Type
    36  		if *decision.Simulated {
    37  			k = fmt.Sprintf("(simul)%s", k)
    38  		}
    39  
    40  		v := decMap[k]
    41  		decMap[k] = v + 1
    42  	}
    43  
    44  	for k, v := range decMap {
    45  		if len(ret) > 0 {
    46  			ret += " "
    47  		}
    48  
    49  		ret += fmt.Sprintf("%s:%d", k, v)
    50  	}
    51  
    52  	return ret
    53  }
    54  
    55  func (cli *cliAlerts) alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
    56  	switch cli.cfg().Cscli.Output {
    57  	case "raw":
    58  		csvwriter := csv.NewWriter(os.Stdout)
    59  		header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
    60  
    61  		if printMachine {
    62  			header = append(header, "machine")
    63  		}
    64  
    65  		if err := csvwriter.Write(header); err != nil {
    66  			return err
    67  		}
    68  
    69  		for _, alertItem := range *alerts {
    70  			row := []string{
    71  				strconv.FormatInt(alertItem.ID, 10),
    72  				*alertItem.Source.Scope,
    73  				*alertItem.Source.Value,
    74  				*alertItem.Scenario,
    75  				alertItem.Source.Cn,
    76  				alertItem.Source.GetAsNumberName(),
    77  				DecisionsFromAlert(alertItem),
    78  				*alertItem.StartAt,
    79  			}
    80  			if printMachine {
    81  				row = append(row, alertItem.MachineID)
    82  			}
    83  
    84  			if err := csvwriter.Write(row); err != nil {
    85  				return err
    86  			}
    87  		}
    88  
    89  		csvwriter.Flush()
    90  	case "json":
    91  		if *alerts == nil {
    92  			// avoid returning "null" in json
    93  			// could be cleaner if we used slice of alerts directly
    94  			fmt.Println("[]")
    95  			return nil
    96  		}
    97  
    98  		x, _ := json.MarshalIndent(alerts, "", " ")
    99  		fmt.Print(string(x))
   100  	case "human":
   101  		if len(*alerts) == 0 {
   102  			fmt.Println("No active alerts")
   103  			return nil
   104  		}
   105  
   106  		alertsTable(color.Output, alerts, printMachine)
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  var alertTemplate = `
   113  ################################################################################################
   114  
   115   - ID           : {{.ID}}
   116   - Date         : {{.CreatedAt}}
   117   - Machine      : {{.MachineID}}
   118   - Simulation   : {{.Simulated}}
   119   - Reason       : {{.Scenario}}
   120   - Events Count : {{.EventsCount}}
   121   - Scope:Value  : {{.Source.Scope}}{{if .Source.Value}}:{{.Source.Value}}{{end}}
   122   - Country      : {{.Source.Cn}}
   123   - AS           : {{.Source.AsName}}
   124   - Begin        : {{.StartAt}}
   125   - End          : {{.StopAt}}
   126   - UUID         : {{.UUID}}
   127  
   128  `
   129  
   130  func (cli *cliAlerts) displayOneAlert(alert *models.Alert, withDetail bool) error {
   131  	tmpl, err := template.New("alert").Parse(alertTemplate)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	if err = tmpl.Execute(os.Stdout, alert); err != nil {
   137  		return err
   138  	}
   139  
   140  	alertDecisionsTable(color.Output, alert)
   141  
   142  	if len(alert.Meta) > 0 {
   143  		fmt.Printf("\n - Context  :\n")
   144  		sort.Slice(alert.Meta, func(i, j int) bool {
   145  			return alert.Meta[i].Key < alert.Meta[j].Key
   146  		})
   147  
   148  		table := newTable(color.Output)
   149  		table.SetRowLines(false)
   150  		table.SetHeaders("Key", "Value")
   151  
   152  		for _, meta := range alert.Meta {
   153  			var valSlice []string
   154  			if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
   155  				return fmt.Errorf("unknown context value type '%s': %w", meta.Value, err)
   156  			}
   157  
   158  			for _, value := range valSlice {
   159  				table.AddRow(
   160  					meta.Key,
   161  					value,
   162  				)
   163  			}
   164  		}
   165  
   166  		table.Render()
   167  	}
   168  
   169  	if withDetail {
   170  		fmt.Printf("\n - Events  :\n")
   171  
   172  		for _, event := range alert.Events {
   173  			alertEventTable(color.Output, event)
   174  		}
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  type cliAlerts struct {
   181  	client *apiclient.ApiClient
   182  	cfg    configGetter
   183  }
   184  
   185  func NewCLIAlerts(getconfig configGetter) *cliAlerts {
   186  	return &cliAlerts{
   187  		cfg: getconfig,
   188  	}
   189  }
   190  
   191  func (cli *cliAlerts) NewCommand() *cobra.Command {
   192  	cmd := &cobra.Command{
   193  		Use:               "alerts [action]",
   194  		Short:             "Manage alerts",
   195  		Args:              cobra.MinimumNArgs(1),
   196  		DisableAutoGenTag: true,
   197  		Aliases:           []string{"alert"},
   198  		PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
   199  			cfg := cli.cfg()
   200  			if err := cfg.LoadAPIClient(); err != nil {
   201  				return fmt.Errorf("loading api client: %w", err)
   202  			}
   203  			apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
   204  			if err != nil {
   205  				return fmt.Errorf("parsing api url %s: %w", apiURL, err)
   206  			}
   207  			cli.client, err = apiclient.NewClient(&apiclient.Config{
   208  				MachineID:     cfg.API.Client.Credentials.Login,
   209  				Password:      strfmt.Password(cfg.API.Client.Credentials.Password),
   210  				UserAgent:     fmt.Sprintf("crowdsec/%s", version.String()),
   211  				URL:           apiURL,
   212  				VersionPrefix: "v1",
   213  			})
   214  
   215  			if err != nil {
   216  				return fmt.Errorf("new api client: %w", err)
   217  			}
   218  
   219  			return nil
   220  		},
   221  	}
   222  
   223  	cmd.AddCommand(cli.NewListCmd())
   224  	cmd.AddCommand(cli.NewInspectCmd())
   225  	cmd.AddCommand(cli.NewFlushCmd())
   226  	cmd.AddCommand(cli.NewDeleteCmd())
   227  
   228  	return cmd
   229  }
   230  
   231  func (cli *cliAlerts) NewListCmd() *cobra.Command {
   232  	var alertListFilter = apiclient.AlertsListOpts{
   233  		ScopeEquals:    new(string),
   234  		ValueEquals:    new(string),
   235  		ScenarioEquals: new(string),
   236  		IPEquals:       new(string),
   237  		RangeEquals:    new(string),
   238  		Since:          new(string),
   239  		Until:          new(string),
   240  		TypeEquals:     new(string),
   241  		IncludeCAPI:    new(bool),
   242  		OriginEquals:   new(string),
   243  	}
   244  
   245  	limit := new(int)
   246  	contained := new(bool)
   247  
   248  	var printMachine bool
   249  
   250  	cmd := &cobra.Command{
   251  		Use:   "list [filters]",
   252  		Short: "List alerts",
   253  		Example: `cscli alerts list
   254  cscli alerts list --ip 1.2.3.4
   255  cscli alerts list --range 1.2.3.0/24
   256  cscli alerts list --origin lists
   257  cscli alerts list -s crowdsecurity/ssh-bf
   258  cscli alerts list --type ban`,
   259  		Long:              `List alerts with optional filters`,
   260  		DisableAutoGenTag: true,
   261  		RunE: func(cmd *cobra.Command, _ []string) error {
   262  			if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
   263  				alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
   264  				printHelp(cmd)
   265  				return err
   266  			}
   267  			if limit != nil {
   268  				alertListFilter.Limit = limit
   269  			}
   270  
   271  			if *alertListFilter.Until == "" {
   272  				alertListFilter.Until = nil
   273  			} else if strings.HasSuffix(*alertListFilter.Until, "d") {
   274  				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
   275  				realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
   276  				days, err := strconv.Atoi(realDuration)
   277  				if err != nil {
   278  					printHelp(cmd)
   279  					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
   280  				}
   281  				*alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
   282  			}
   283  			if *alertListFilter.Since == "" {
   284  				alertListFilter.Since = nil
   285  			} else if strings.HasSuffix(*alertListFilter.Since, "d") {
   286  				/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
   287  				realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
   288  				days, err := strconv.Atoi(realDuration)
   289  				if err != nil {
   290  					printHelp(cmd)
   291  					return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
   292  				}
   293  				*alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
   294  			}
   295  
   296  			if *alertListFilter.IncludeCAPI {
   297  				*alertListFilter.Limit = 0
   298  			}
   299  
   300  			if *alertListFilter.TypeEquals == "" {
   301  				alertListFilter.TypeEquals = nil
   302  			}
   303  			if *alertListFilter.ScopeEquals == "" {
   304  				alertListFilter.ScopeEquals = nil
   305  			}
   306  			if *alertListFilter.ValueEquals == "" {
   307  				alertListFilter.ValueEquals = nil
   308  			}
   309  			if *alertListFilter.ScenarioEquals == "" {
   310  				alertListFilter.ScenarioEquals = nil
   311  			}
   312  			if *alertListFilter.IPEquals == "" {
   313  				alertListFilter.IPEquals = nil
   314  			}
   315  			if *alertListFilter.RangeEquals == "" {
   316  				alertListFilter.RangeEquals = nil
   317  			}
   318  
   319  			if *alertListFilter.OriginEquals == "" {
   320  				alertListFilter.OriginEquals = nil
   321  			}
   322  
   323  			if contained != nil && *contained {
   324  				alertListFilter.Contains = new(bool)
   325  			}
   326  
   327  			alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
   328  			if err != nil {
   329  				return fmt.Errorf("unable to list alerts: %w", err)
   330  			}
   331  
   332  			if err = cli.alertsToTable(alerts, printMachine); err != nil {
   333  				return fmt.Errorf("unable to list alerts: %w", err)
   334  			}
   335  
   336  			return nil
   337  		},
   338  	}
   339  
   340  	flags := cmd.Flags()
   341  	flags.SortFlags = false
   342  	flags.BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
   343  	flags.StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
   344  	flags.StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
   345  	flags.StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
   346  	flags.StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
   347  	flags.StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
   348  	flags.StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
   349  	flags.StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
   350  	flags.StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
   351  	flags.StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
   352  	flags.BoolVar(contained, "contained", false, "query decisions contained by range")
   353  	flags.BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
   354  	flags.IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
   355  
   356  	return cmd
   357  }
   358  
   359  func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
   360  	var (
   361  		ActiveDecision *bool
   362  		AlertDeleteAll bool
   363  		delAlertByID   string
   364  	)
   365  
   366  	var alertDeleteFilter = apiclient.AlertsDeleteOpts{
   367  		ScopeEquals:    new(string),
   368  		ValueEquals:    new(string),
   369  		ScenarioEquals: new(string),
   370  		IPEquals:       new(string),
   371  		RangeEquals:    new(string),
   372  	}
   373  
   374  	contained := new(bool)
   375  
   376  	cmd := &cobra.Command{
   377  		Use: "delete [filters] [--all]",
   378  		Short: `Delete alerts
   379  /!\ This command can be use only on the same machine than the local API.`,
   380  		Example: `cscli alerts delete --ip 1.2.3.4
   381  cscli alerts delete --range 1.2.3.0/24
   382  cscli alerts delete -s crowdsecurity/ssh-bf"`,
   383  		DisableAutoGenTag: true,
   384  		Aliases:           []string{"remove"},
   385  		Args:              cobra.ExactArgs(0),
   386  		PreRunE: func(cmd *cobra.Command, _ []string) error {
   387  			if AlertDeleteAll {
   388  				return nil
   389  			}
   390  			if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" &&
   391  				*alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
   392  				*alertDeleteFilter.RangeEquals == "" && delAlertByID == "" {
   393  				_ = cmd.Usage()
   394  				return fmt.Errorf("at least one filter or --all must be specified")
   395  			}
   396  
   397  			return nil
   398  		},
   399  		RunE: func(cmd *cobra.Command, _ []string) error {
   400  			var err error
   401  
   402  			if !AlertDeleteAll {
   403  				if err = manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
   404  					alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
   405  					printHelp(cmd)
   406  					return err
   407  				}
   408  				if ActiveDecision != nil {
   409  					alertDeleteFilter.ActiveDecisionEquals = ActiveDecision
   410  				}
   411  
   412  				if *alertDeleteFilter.ScopeEquals == "" {
   413  					alertDeleteFilter.ScopeEquals = nil
   414  				}
   415  				if *alertDeleteFilter.ValueEquals == "" {
   416  					alertDeleteFilter.ValueEquals = nil
   417  				}
   418  				if *alertDeleteFilter.ScenarioEquals == "" {
   419  					alertDeleteFilter.ScenarioEquals = nil
   420  				}
   421  				if *alertDeleteFilter.IPEquals == "" {
   422  					alertDeleteFilter.IPEquals = nil
   423  				}
   424  				if *alertDeleteFilter.RangeEquals == "" {
   425  					alertDeleteFilter.RangeEquals = nil
   426  				}
   427  				if contained != nil && *contained {
   428  					alertDeleteFilter.Contains = new(bool)
   429  				}
   430  				limit := 0
   431  				alertDeleteFilter.Limit = &limit
   432  			} else {
   433  				limit := 0
   434  				alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit}
   435  			}
   436  
   437  			var alerts *models.DeleteAlertsResponse
   438  			if delAlertByID == "" {
   439  				alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
   440  				if err != nil {
   441  					return fmt.Errorf("unable to delete alerts: %w", err)
   442  				}
   443  			} else {
   444  				alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
   445  				if err != nil {
   446  					return fmt.Errorf("unable to delete alert: %w", err)
   447  				}
   448  			}
   449  			log.Infof("%s alert(s) deleted", alerts.NbDeleted)
   450  
   451  			return nil
   452  		},
   453  	}
   454  
   455  	flags := cmd.Flags()
   456  	flags.SortFlags = false
   457  	flags.StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
   458  	flags.StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
   459  	flags.StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
   460  	flags.StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
   461  	flags.StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
   462  	flags.StringVar(&delAlertByID, "id", "", "alert ID")
   463  	flags.BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
   464  	flags.BoolVar(contained, "contained", false, "query decisions contained by range")
   465  
   466  	return cmd
   467  }
   468  
   469  func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
   470  	var details bool
   471  
   472  	cmd := &cobra.Command{
   473  		Use:               `inspect "alert_id"`,
   474  		Short:             `Show info about an alert`,
   475  		Example:           `cscli alerts inspect 123`,
   476  		DisableAutoGenTag: true,
   477  		RunE: func(cmd *cobra.Command, args []string) error {
   478  			cfg := cli.cfg()
   479  			if len(args) == 0 {
   480  				printHelp(cmd)
   481  				return fmt.Errorf("missing alert_id")
   482  			}
   483  			for _, alertID := range args {
   484  				id, err := strconv.Atoi(alertID)
   485  				if err != nil {
   486  					return fmt.Errorf("bad alert id %s", alertID)
   487  				}
   488  				alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
   489  				if err != nil {
   490  					return fmt.Errorf("can't find alert with id %s: %w", alertID, err)
   491  				}
   492  				switch cfg.Cscli.Output {
   493  				case "human":
   494  					if err := cli.displayOneAlert(alert, details); err != nil {
   495  						continue
   496  					}
   497  				case "json":
   498  					data, err := json.MarshalIndent(alert, "", "  ")
   499  					if err != nil {
   500  						return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
   501  					}
   502  					fmt.Printf("%s\n", string(data))
   503  				case "raw":
   504  					data, err := yaml.Marshal(alert)
   505  					if err != nil {
   506  						return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
   507  					}
   508  					fmt.Println(string(data))
   509  				}
   510  			}
   511  
   512  			return nil
   513  		},
   514  	}
   515  
   516  	cmd.Flags().SortFlags = false
   517  	cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
   518  
   519  	return cmd
   520  }
   521  
   522  func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
   523  	var (
   524  		maxItems int
   525  		maxAge   string
   526  	)
   527  
   528  	cmd := &cobra.Command{
   529  		Use: `flush`,
   530  		Short: `Flush alerts
   531  /!\ This command can be used only on the same machine than the local API`,
   532  		Example:           `cscli alerts flush --max-items 1000 --max-age 7d`,
   533  		DisableAutoGenTag: true,
   534  		RunE: func(_ *cobra.Command, _ []string) error {
   535  			cfg := cli.cfg()
   536  			if err := require.LAPI(cfg); err != nil {
   537  				return err
   538  			}
   539  			db, err := database.NewClient(cfg.DbConfig)
   540  			if err != nil {
   541  				return fmt.Errorf("unable to create new database client: %w", err)
   542  			}
   543  			log.Info("Flushing alerts. !! This may take a long time !!")
   544  			err = db.FlushAlerts(maxAge, maxItems)
   545  			if err != nil {
   546  				return fmt.Errorf("unable to flush alerts: %w", err)
   547  			}
   548  			log.Info("Alerts flushed")
   549  
   550  			return nil
   551  		},
   552  	}
   553  
   554  	cmd.Flags().SortFlags = false
   555  	cmd.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
   556  	cmd.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")
   557  
   558  	return cmd
   559  }