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 }