package main import ( "context" "encoding/csv" "encoding/json" "fmt" "net/url" "os" "sort" "strconv" "strings" "text/template" "time" "github.com/fatih/color" "github.com/go-openapi/strfmt" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/yaml.v2" "github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" ) func DecisionsFromAlert(alert *models.Alert) string { ret := "" var decMap = make(map[string]int) for _, decision := range alert.Decisions { k := *decision.Type if *decision.Simulated { k = fmt.Sprintf("(simul)%s", k) } v := decMap[k] decMap[k] = v + 1 } for k, v := range decMap { if len(ret) > 0 { ret += " " } ret += fmt.Sprintf("%s:%d", k, v) } return ret } func DateFromAlert(alert *models.Alert) string { ts, err := time.Parse(time.RFC3339, alert.CreatedAt) if err != nil { log.Infof("while parsing %s with %s : %s", alert.CreatedAt, time.RFC3339, err) return alert.CreatedAt } return ts.Format(time.RFC822) } func SourceFromAlert(alert *models.Alert) string { //more than one item, just number and scope if len(alert.Decisions) > 1 { return fmt.Sprintf("%d %ss (%s)", len(alert.Decisions), *alert.Decisions[0].Scope, *alert.Decisions[0].Origin) } //fallback on single decision information if len(alert.Decisions) == 1 { return fmt.Sprintf("%s:%s", *alert.Decisions[0].Scope, *alert.Decisions[0].Value) } //try to compose a human friendly version if *alert.Source.Value != "" && *alert.Source.Scope != "" { scope := "" scope = fmt.Sprintf("%s:%s", *alert.Source.Scope, *alert.Source.Value) extra := "" if alert.Source.Cn != "" { extra = alert.Source.Cn } if alert.Source.AsNumber != "" { extra += fmt.Sprintf("/%s", alert.Source.AsNumber) } if alert.Source.AsName != "" { extra += fmt.Sprintf("/%s", alert.Source.AsName) } if extra != "" { scope += " (" + extra + ")" } return scope } return "" } func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error { if csConfig.Cscli.Output == "raw" { csvwriter := csv.NewWriter(os.Stdout) header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"} if printMachine { header = append(header, "machine") } err := csvwriter.Write(header) if err != nil { return err } for _, alertItem := range *alerts { row := []string{ fmt.Sprintf("%d", alertItem.ID), *alertItem.Source.Scope, *alertItem.Source.Value, *alertItem.Scenario, alertItem.Source.Cn, alertItem.Source.GetAsNumberName(), DecisionsFromAlert(alertItem), *alertItem.StartAt, } if printMachine { row = append(row, alertItem.MachineID) } err := csvwriter.Write(row) if err != nil { return err } } csvwriter.Flush() } else if csConfig.Cscli.Output == "json" { if *alerts == nil { // avoid returning "null" in json // could be cleaner if we used slice of alerts directly fmt.Println("[]") return nil } x, _ := json.MarshalIndent(alerts, "", " ") fmt.Printf("%s", string(x)) } else if csConfig.Cscli.Output == "human" { if len(*alerts) == 0 { fmt.Println("No active alerts") return nil } alertsTable(color.Output, alerts, printMachine) } return nil } var alertTemplate = ` ################################################################################################ - ID : {{.ID}} - Date : {{.CreatedAt}} - Machine : {{.MachineID}} - Simulation : {{.Simulated}} - Reason : {{.Scenario}} - Events Count : {{.EventsCount}} - Scope:Value : {{.Source.Scope}}{{if .Source.Value}}:{{.Source.Value}}{{end}} - Country : {{.Source.Cn}} - AS : {{.Source.AsName}} - Begin : {{.StartAt}} - End : {{.StopAt}} - UUID : {{.UUID}} ` func DisplayOneAlert(alert *models.Alert, withDetail bool) error { if csConfig.Cscli.Output == "human" { tmpl, err := template.New("alert").Parse(alertTemplate) if err != nil { return err } err = tmpl.Execute(os.Stdout, alert) if err != nil { return err } alertDecisionsTable(color.Output, alert) if len(alert.Meta) > 0 { fmt.Printf("\n - Context :\n") sort.Slice(alert.Meta, func(i, j int) bool { return alert.Meta[i].Key < alert.Meta[j].Key }) table := newTable(color.Output) table.SetRowLines(false) table.SetHeaders("Key", "Value") for _, meta := range alert.Meta { var valSlice []string if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil { return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err) } for _, value := range valSlice { table.AddRow( meta.Key, value, ) } } table.Render() } if withDetail { fmt.Printf("\n - Events :\n") for _, event := range alert.Events { alertEventTable(color.Output, event) } } } return nil } func NewAlertsCmd() *cobra.Command { var cmdAlerts = &cobra.Command{ Use: "alerts [action]", Short: "Manage alerts", Args: cobra.MinimumNArgs(1), DisableAutoGenTag: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { var err error if err := csConfig.LoadAPIClient(); err != nil { return fmt.Errorf("loading api client: %w", err) } apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL) if err != nil { return fmt.Errorf("parsing api url %s: %w", apiURL, err) } Client, err = apiclient.NewClient(&apiclient.Config{ MachineID: csConfig.API.Client.Credentials.Login, Password: strfmt.Password(csConfig.API.Client.Credentials.Password), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), URL: apiURL, VersionPrefix: "v1", }) if err != nil { return fmt.Errorf("new api client: %w", err) } return nil }, } cmdAlerts.AddCommand(NewAlertsListCmd()) cmdAlerts.AddCommand(NewAlertsInspectCmd()) cmdAlerts.AddCommand(NewAlertsFlushCmd()) cmdAlerts.AddCommand(NewAlertsDeleteCmd()) return cmdAlerts } func NewAlertsListCmd() *cobra.Command { var alertListFilter = apiclient.AlertsListOpts{ ScopeEquals: new(string), ValueEquals: new(string), ScenarioEquals: new(string), IPEquals: new(string), RangeEquals: new(string), Since: new(string), Until: new(string), TypeEquals: new(string), IncludeCAPI: new(bool), OriginEquals: new(string), } var limit = new(int) contained := new(bool) var printMachine bool var cmdAlertsList = &cobra.Command{ Use: "list [filters]", Short: "List alerts", Example: `cscli alerts list cscli alerts list --ip 1.2.3.4 cscli alerts list --range 1.2.3.0/24 cscli alerts list -s crowdsecurity/ssh-bf cscli alerts list --type ban`, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { var err error if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals, alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil { printHelp(cmd) return err } if limit != nil { alertListFilter.Limit = limit } if *alertListFilter.Until == "" { alertListFilter.Until = nil } else if strings.HasSuffix(*alertListFilter.Until, "d") { /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/ realDuration := strings.TrimSuffix(*alertListFilter.Until, "d") days, err := strconv.Atoi(realDuration) if err != nil { printHelp(cmd) return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until) } *alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h") } if *alertListFilter.Since == "" { alertListFilter.Since = nil } else if strings.HasSuffix(*alertListFilter.Since, "d") { /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/ realDuration := strings.TrimSuffix(*alertListFilter.Since, "d") days, err := strconv.Atoi(realDuration) if err != nil { printHelp(cmd) return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since) } *alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h") } if *alertListFilter.IncludeCAPI { *alertListFilter.Limit = 0 } if *alertListFilter.TypeEquals == "" { alertListFilter.TypeEquals = nil } if *alertListFilter.ScopeEquals == "" { alertListFilter.ScopeEquals = nil } if *alertListFilter.ValueEquals == "" { alertListFilter.ValueEquals = nil } if *alertListFilter.ScenarioEquals == "" { alertListFilter.ScenarioEquals = nil } if *alertListFilter.IPEquals == "" { alertListFilter.IPEquals = nil } if *alertListFilter.RangeEquals == "" { alertListFilter.RangeEquals = nil } if *alertListFilter.OriginEquals == "" { alertListFilter.OriginEquals = nil } if contained != nil && *contained { alertListFilter.Contains = new(bool) } alerts, _, err := Client.Alerts.List(context.Background(), alertListFilter) if err != nil { return fmt.Errorf("unable to list alerts: %v", err) } err = AlertsToTable(alerts, printMachine) if err != nil { return fmt.Errorf("unable to list alerts: %v", err) } return nil }, } cmdAlertsList.Flags().SortFlags = false cmdAlertsList.Flags().BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API") cmdAlertsList.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)") cmdAlertsList.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)") cmdAlertsList.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value )") cmdAlertsList.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)") cmdAlertsList.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value )") cmdAlertsList.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)") cmdAlertsList.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)") cmdAlertsList.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope") cmdAlertsList.Flags().StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ","))) cmdAlertsList.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") cmdAlertsList.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts") cmdAlertsList.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)") return cmdAlertsList } func NewAlertsDeleteCmd() *cobra.Command { var ActiveDecision *bool var AlertDeleteAll bool var delAlertByID string contained := new(bool) var alertDeleteFilter = apiclient.AlertsDeleteOpts{ ScopeEquals: new(string), ValueEquals: new(string), ScenarioEquals: new(string), IPEquals: new(string), RangeEquals: new(string), } var cmdAlertsDelete = &cobra.Command{ Use: "delete [filters] [--all]", Short: `Delete alerts /!\ This command can be use only on the same machine than the local API.`, Example: `cscli alerts delete --ip 1.2.3.4 cscli alerts delete --range 1.2.3.0/24 cscli alerts delete -s crowdsecurity/ssh-bf"`, DisableAutoGenTag: true, Aliases: []string{"remove"}, Args: cobra.ExactArgs(0), PreRunE: func(cmd *cobra.Command, args []string) error { if AlertDeleteAll { return nil } if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" && *alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" && *alertDeleteFilter.RangeEquals == "" && delAlertByID == "" { _ = cmd.Usage() return fmt.Errorf("at least one filter or --all must be specified") } return nil }, RunE: func(cmd *cobra.Command, args []string) error { var err error if !AlertDeleteAll { if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals, alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil { printHelp(cmd) return err } if ActiveDecision != nil { alertDeleteFilter.ActiveDecisionEquals = ActiveDecision } if *alertDeleteFilter.ScopeEquals == "" { alertDeleteFilter.ScopeEquals = nil } if *alertDeleteFilter.ValueEquals == "" { alertDeleteFilter.ValueEquals = nil } if *alertDeleteFilter.ScenarioEquals == "" { alertDeleteFilter.ScenarioEquals = nil } if *alertDeleteFilter.IPEquals == "" { alertDeleteFilter.IPEquals = nil } if *alertDeleteFilter.RangeEquals == "" { alertDeleteFilter.RangeEquals = nil } if contained != nil && *contained { alertDeleteFilter.Contains = new(bool) } limit := 0 alertDeleteFilter.Limit = &limit } else { limit := 0 alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit} } var alerts *models.DeleteAlertsResponse if delAlertByID == "" { alerts, _, err = Client.Alerts.Delete(context.Background(), alertDeleteFilter) if err != nil { return fmt.Errorf("unable to delete alerts : %v", err) } } else { alerts, _, err = Client.Alerts.DeleteOne(context.Background(), delAlertByID) if err != nil { return fmt.Errorf("unable to delete alert: %v", err) } } log.Infof("%s alert(s) deleted", alerts.NbDeleted) return nil }, } cmdAlertsDelete.Flags().SortFlags = false cmdAlertsDelete.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)") cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope") cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)") cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value )") cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value )") cmdAlertsDelete.Flags().StringVar(&delAlertByID, "id", "", "alert ID") cmdAlertsDelete.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts") cmdAlertsDelete.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") return cmdAlertsDelete } func NewAlertsInspectCmd() *cobra.Command { var details bool var cmdAlertsInspect = &cobra.Command{ Use: `inspect "alert_id"`, Short: `Show info about an alert`, Example: `cscli alerts inspect 123`, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { printHelp(cmd) return fmt.Errorf("missing alert_id") } for _, alertID := range args { id, err := strconv.Atoi(alertID) if err != nil { return fmt.Errorf("bad alert id %s", alertID) } alert, _, err := Client.Alerts.GetByID(context.Background(), id) if err != nil { return fmt.Errorf("can't find alert with id %s: %s", alertID, err) } switch csConfig.Cscli.Output { case "human": if err := DisplayOneAlert(alert, details); err != nil { continue } case "json": data, err := json.MarshalIndent(alert, "", " ") if err != nil { return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err) } fmt.Printf("%s\n", string(data)) case "raw": data, err := yaml.Marshal(alert) if err != nil { return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err) } fmt.Printf("%s\n", string(data)) } } return nil }, } cmdAlertsInspect.Flags().SortFlags = false cmdAlertsInspect.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events") return cmdAlertsInspect } func NewAlertsFlushCmd() *cobra.Command { var maxItems int var maxAge string var cmdAlertsFlush = &cobra.Command{ Use: `flush`, Short: `Flush alerts /!\ This command can be used only on the same machine than the local API`, Example: `cscli alerts flush --max-items 1000 --max-age 7d`, DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { var err error if err := require.LAPI(csConfig); err != nil { return err } dbClient, err = database.NewClient(csConfig.DbConfig) if err != nil { return fmt.Errorf("unable to create new database client: %s", err) } log.Info("Flushing alerts. !! This may take a long time !!") err = dbClient.FlushAlerts(maxAge, maxItems) if err != nil { return fmt.Errorf("unable to flush alerts: %s", err) } log.Info("Alerts flushed") return nil }, } cmdAlertsFlush.Flags().SortFlags = false cmdAlertsFlush.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database") cmdAlertsFlush.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database") return cmdAlertsFlush }