123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- 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,
- Aliases: []string{"alert"},
- 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 <IP>)")
- 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 <RANGE/X>)")
- 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 <IP>)")
- cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
- 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
- }
|