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 }