bitbucket.org/Aishee/synsec@v0.0.0-20210414005726-236fc01a153d/cmd/synsec-cli/decisions.go (about) 1 package main 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/url" 8 "os" 9 "strconv" 10 "strings" 11 "time" 12 13 "bitbucket.org/Aishee/synsec/pkg/apiclient" 14 "bitbucket.org/Aishee/synsec/pkg/cwversion" 15 "bitbucket.org/Aishee/synsec/pkg/models" 16 "bitbucket.org/Aishee/synsec/pkg/types" 17 "github.com/go-openapi/strfmt" 18 "github.com/olekukonko/tablewriter" 19 log "github.com/sirupsen/logrus" 20 "github.com/spf13/cobra" 21 ) 22 23 var Client *apiclient.ApiClient 24 25 func DecisionsToTable(alerts *models.GetAlertsResponse) error { 26 /*here we cheat a bit : to make it more readable for the user, we dedup some entries*/ 27 var spamLimit map[string]bool = make(map[string]bool) 28 29 /*process in reverse order to keep the latest item only*/ 30 for aIdx := len(*alerts) - 1; aIdx >= 0; aIdx-- { 31 alertItem := (*alerts)[aIdx] 32 newDecisions := make([]*models.Decision, 0) 33 for _, decisionItem := range alertItem.Decisions { 34 spamKey := fmt.Sprintf("%t:%s:%s:%s", *decisionItem.Simulated, *decisionItem.Type, *decisionItem.Scope, *decisionItem.Value) 35 if _, ok := spamLimit[spamKey]; ok { 36 continue 37 } 38 spamLimit[spamKey] = true 39 newDecisions = append(newDecisions, decisionItem) 40 } 41 alertItem.Decisions = newDecisions 42 } 43 if csConfig.Cscli.Output == "raw" { 44 fmt.Printf("id,source,ip,reason,action,country,as,events_count,expiration,simulated,alert_id\n") 45 for _, alertItem := range *alerts { 46 for _, decisionItem := range alertItem.Decisions { 47 fmt.Printf("%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,%v\n", 48 decisionItem.ID, 49 *decisionItem.Origin, 50 *decisionItem.Scope+":"+*decisionItem.Value, 51 *decisionItem.Scenario, 52 *decisionItem.Type, 53 alertItem.Source.Cn, 54 alertItem.Source.AsNumber+" "+alertItem.Source.AsName, 55 *alertItem.EventsCount, 56 *decisionItem.Duration, 57 *decisionItem.Simulated, 58 alertItem.ID) 59 } 60 } 61 } else if csConfig.Cscli.Output == "json" { 62 x, _ := json.MarshalIndent(alerts, "", " ") 63 fmt.Printf("%s", string(x)) 64 } else if csConfig.Cscli.Output == "human" { 65 66 table := tablewriter.NewWriter(os.Stdout) 67 table.SetHeader([]string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"}) 68 69 if len(*alerts) == 0 { 70 fmt.Println("No active decisions") 71 return nil 72 } 73 74 for _, alertItem := range *alerts { 75 for _, decisionItem := range alertItem.Decisions { 76 if *alertItem.Simulated { 77 *decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type) 78 } 79 table.Append([]string{ 80 strconv.Itoa(int(decisionItem.ID)), 81 *decisionItem.Origin, 82 *decisionItem.Scope + ":" + *decisionItem.Value, 83 *decisionItem.Scenario, 84 *decisionItem.Type, 85 alertItem.Source.Cn, 86 alertItem.Source.AsNumber + " " + alertItem.Source.AsName, 87 strconv.Itoa(int(*alertItem.EventsCount)), 88 *decisionItem.Duration, 89 strconv.Itoa(int(alertItem.ID)), 90 }) 91 } 92 } 93 table.Render() // Send output 94 } 95 return nil 96 } 97 98 func NewDecisionsCmd() *cobra.Command { 99 /* ---- DECISIONS COMMAND */ 100 var cmdDecisions = &cobra.Command{ 101 Use: "decisions [action]", 102 Short: "Manage decisions", 103 Long: `Add/List/Delete decisions from LAPI`, 104 Example: `ccscli decisions [action] [filter]`, 105 /*TBD example*/ 106 Args: cobra.MinimumNArgs(1), 107 PersistentPreRun: func(cmd *cobra.Command, args []string) { 108 if err := csConfig.LoadAPIClient(); err != nil { 109 log.Fatalf(err.Error()) 110 } 111 if csConfig.API.Client == nil { 112 log.Fatalln("There is no configuration on 'api_client:'") 113 } 114 if csConfig.API.Client.Credentials == nil { 115 log.Fatalf("Please provide credentials for the API in '%s'", csConfig.API.Client.CredentialsFilePath) 116 } 117 password := strfmt.Password(csConfig.API.Client.Credentials.Password) 118 apiurl, err := url.Parse(csConfig.API.Client.Credentials.URL) 119 if err != nil { 120 log.Fatalf("parsing api url ('%s'): %s", csConfig.API.Client.Credentials.URL, err) 121 } 122 Client, err = apiclient.NewClient(&apiclient.Config{ 123 MachineID: csConfig.API.Client.Credentials.Login, 124 Password: password, 125 UserAgent: fmt.Sprintf("synsec/%s", cwversion.VersionStr()), 126 URL: apiurl, 127 VersionPrefix: "v1", 128 }) 129 if err != nil { 130 log.Fatalf("creating api client : %s", err) 131 } 132 }, 133 } 134 135 var filter = apiclient.AlertsListOpts{ 136 ValueEquals: new(string), 137 ScopeEquals: new(string), 138 ScenarioEquals: new(string), 139 IPEquals: new(string), 140 RangeEquals: new(string), 141 Since: new(string), 142 Until: new(string), 143 TypeEquals: new(string), 144 } 145 NoSimu := new(bool) 146 contained := new(bool) 147 var cmdDecisionsList = &cobra.Command{ 148 Use: "list [options]", 149 Short: "List decisions from LAPI", 150 Example: `ccscli decisions list -i 1.2.3.4 151 ccscli decisions list -r 1.2.3.0/24 152 ccscli decisions list -s breakteam/ssh-bf 153 ccscli decisions list -t ban 154 `, 155 Args: cobra.ExactArgs(0), 156 Run: func(cmd *cobra.Command, args []string) { 157 var err error 158 /*take care of shorthand options*/ 159 if err := manageCliDecisionAlerts(filter.IPEquals, filter.RangeEquals, filter.ScopeEquals, filter.ValueEquals); err != nil { 160 log.Fatalf("%s", err) 161 } 162 filter.ActiveDecisionEquals = new(bool) 163 *filter.ActiveDecisionEquals = true 164 if NoSimu != nil && *NoSimu { 165 filter.IncludeSimulated = new(bool) 166 } 167 /*nulify the empty entries to avoid bad filter*/ 168 if *filter.Until == "" { 169 filter.Until = nil 170 } else { 171 /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/ 172 if strings.HasSuffix(*filter.Until, "d") { 173 realDuration := strings.TrimSuffix(*filter.Until, "d") 174 days, err := strconv.Atoi(realDuration) 175 if err != nil { 176 cmd.Help() 177 log.Fatalf("Can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Until) 178 } 179 *filter.Until = fmt.Sprintf("%d%s", days*24, "h") 180 } 181 } 182 if *filter.Since == "" { 183 filter.Since = nil 184 } else { 185 /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/ 186 if strings.HasSuffix(*filter.Since, "d") { 187 realDuration := strings.TrimSuffix(*filter.Since, "d") 188 days, err := strconv.Atoi(realDuration) 189 if err != nil { 190 cmd.Help() 191 log.Fatalf("Can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Until) 192 } 193 *filter.Since = fmt.Sprintf("%d%s", days*24, "h") 194 } 195 } 196 if *filter.TypeEquals == "" { 197 filter.TypeEquals = nil 198 } 199 if *filter.ValueEquals == "" { 200 filter.ValueEquals = nil 201 } 202 if *filter.ScopeEquals == "" { 203 filter.ScopeEquals = nil 204 } 205 if *filter.ScenarioEquals == "" { 206 filter.ScenarioEquals = nil 207 } 208 if *filter.IPEquals == "" { 209 filter.IPEquals = nil 210 } 211 if *filter.RangeEquals == "" { 212 filter.RangeEquals = nil 213 } 214 215 if contained != nil && *contained { 216 filter.Contains = new(bool) 217 } 218 219 alerts, _, err := Client.Alerts.List(context.Background(), filter) 220 if err != nil { 221 log.Fatalf("Unable to list decisions : %v", err.Error()) 222 } 223 224 err = DecisionsToTable(alerts) 225 if err != nil { 226 log.Fatalf("unable to list decisions : %v", err.Error()) 227 } 228 }, 229 } 230 cmdDecisionsList.Flags().SortFlags = false 231 cmdDecisionsList.Flags().StringVar(filter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)") 232 cmdDecisionsList.Flags().StringVar(filter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)") 233 cmdDecisionsList.Flags().StringVarP(filter.TypeEquals, "type", "t", "", "restrict to this decision type (ie. ban,captcha)") 234 cmdDecisionsList.Flags().StringVar(filter.ScopeEquals, "scope", "", "restrict to this scope (ie. ip,range,session)") 235 cmdDecisionsList.Flags().StringVarP(filter.ValueEquals, "value", "v", "", "restrict to this value (ie. 1.2.3.4,userName)") 236 cmdDecisionsList.Flags().StringVarP(filter.ScenarioEquals, "scenario", "s", "", "restrict to this scenario (ie. breakteam/ssh-bf)") 237 cmdDecisionsList.Flags().StringVarP(filter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)") 238 cmdDecisionsList.Flags().StringVarP(filter.RangeEquals, "range", "r", "", "restrict to alerts from this source range (shorthand for --scope range --value <RANGE>)") 239 cmdDecisionsList.Flags().BoolVar(NoSimu, "no-simu", false, "exclude decisions in simulation mode") 240 cmdDecisionsList.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") 241 242 cmdDecisions.AddCommand(cmdDecisionsList) 243 244 var ( 245 addIP string 246 addRange string 247 addDuration string 248 addValue string 249 addScope string 250 addReason string 251 addType string 252 ) 253 254 var cmdDecisionsAdd = &cobra.Command{ 255 Use: "add [options]", 256 Short: "Add decision to LAPI", 257 Example: `ccscli decisions add --ip 1.2.3.4 258 ccscli decisions add --range 1.2.3.0/24 259 ccscli decisions add --ip 1.2.3.4 --duration 24h --type captcha 260 ccscli decisions add --scope username --value foobar 261 `, 262 /*TBD : fix long and example*/ 263 Args: cobra.ExactArgs(0), 264 Run: func(cmd *cobra.Command, args []string) { 265 var err error 266 var ip, ipRange string 267 alerts := models.AddAlertsRequest{} 268 origin := "ccscli" 269 capacity := int32(0) 270 leakSpeed := "0" 271 eventsCount := int32(1) 272 empty := "" 273 simulated := false 274 startAt := time.Now().Format(time.RFC3339) 275 stopAt := time.Now().Format(time.RFC3339) 276 createdAt := time.Now().Format(time.RFC3339) 277 278 /*take care of shorthand options*/ 279 if err := manageCliDecisionAlerts(&addIP, &addRange, &addScope, &addValue); err != nil { 280 log.Fatalf("%s", err) 281 } 282 283 if addIP != "" { 284 addValue = addIP 285 addScope = types.Ip 286 } else if addRange != "" { 287 addValue = addRange 288 addScope = types.Range 289 } else if addValue == "" { 290 cmd.Help() 291 log.Errorf("Missing arguments, a value is required (--ip, --range or --scope and --value)") 292 return 293 } 294 295 if addReason == "" { 296 addReason = fmt.Sprintf("manual '%s' from '%s'", addType, csConfig.API.Client.Credentials.Login) 297 } 298 299 decision := models.Decision{ 300 Duration: &addDuration, 301 Scope: &addScope, 302 Value: &addValue, 303 Type: &addType, 304 Scenario: &addReason, 305 Origin: &origin, 306 } 307 alert := models.Alert{ 308 Capacity: &capacity, 309 Decisions: []*models.Decision{&decision}, 310 Events: []*models.Event{}, 311 EventsCount: &eventsCount, 312 Leakspeed: &leakSpeed, 313 Message: &addReason, 314 ScenarioHash: &empty, 315 Scenario: &addReason, 316 ScenarioVersion: &empty, 317 Simulated: &simulated, 318 Source: &models.Source{ 319 AsName: empty, 320 AsNumber: empty, 321 Cn: empty, 322 IP: ip, 323 Range: ipRange, 324 Scope: &addScope, 325 Value: &addValue, 326 }, 327 StartAt: &startAt, 328 StopAt: &stopAt, 329 CreatedAt: createdAt, 330 } 331 alerts = append(alerts, &alert) 332 333 _, _, err = Client.Alerts.Add(context.Background(), alerts) 334 if err != nil { 335 log.Fatalf(err.Error()) 336 } 337 338 log.Info("Decision successfully added") 339 }, 340 } 341 342 cmdDecisionsAdd.Flags().SortFlags = false 343 cmdDecisionsAdd.Flags().StringVarP(&addIP, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)") 344 cmdDecisionsAdd.Flags().StringVarP(&addRange, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)") 345 cmdDecisionsAdd.Flags().StringVarP(&addDuration, "duration", "d", "4h", "Decision duration (ie. 1h,4h,30m)") 346 cmdDecisionsAdd.Flags().StringVarP(&addValue, "value", "v", "", "The value (ie. --scope username --value foobar)") 347 cmdDecisionsAdd.Flags().StringVar(&addScope, "scope", types.Ip, "Decision scope (ie. ip,range,username)") 348 cmdDecisionsAdd.Flags().StringVarP(&addReason, "reason", "R", "", "Decision reason (ie. scenario-name)") 349 cmdDecisionsAdd.Flags().StringVarP(&addType, "type", "t", "ban", "Decision type (ie. ban,captcha,throttle)") 350 cmdDecisions.AddCommand(cmdDecisionsAdd) 351 352 var delFilter = apiclient.DecisionsDeleteOpts{ 353 ScopeEquals: new(string), 354 ValueEquals: new(string), 355 TypeEquals: new(string), 356 IPEquals: new(string), 357 RangeEquals: new(string), 358 } 359 var delDecisionId string 360 var delDecisionAll bool 361 var cmdDecisionsDelete = &cobra.Command{ 362 Use: "delete [options]", 363 Short: "Delete decisions", 364 Example: `ccscli decisions delete -r 1.2.3.0/24 365 ccscli decisions delete -i 1.2.3.4 366 ccscli decisions delete -s breakteam/ssh-bf 367 ccscli decisions delete --id 42 368 ccscli decisions delete --type captcha 369 `, 370 /*TBD : refaire le Long/Example*/ 371 PreRun: func(cmd *cobra.Command, args []string) { 372 if delDecisionAll { 373 return 374 } 375 if *delFilter.ScopeEquals == "" && *delFilter.ValueEquals == "" && 376 *delFilter.TypeEquals == "" && *delFilter.IPEquals == "" && 377 *delFilter.RangeEquals == "" && delDecisionId == "" { 378 cmd.Usage() 379 log.Fatalln("At least one filter or --all must be specified") 380 } 381 }, 382 Run: func(cmd *cobra.Command, args []string) { 383 var err error 384 var decisions *models.DeleteDecisionResponse 385 386 /*take care of shorthand options*/ 387 if err := manageCliDecisionAlerts(delFilter.IPEquals, delFilter.RangeEquals, delFilter.ScopeEquals, delFilter.ValueEquals); err != nil { 388 log.Fatalf("%s", err) 389 } 390 if *delFilter.ScopeEquals == "" { 391 delFilter.ScopeEquals = nil 392 } 393 if *delFilter.ValueEquals == "" { 394 delFilter.ValueEquals = nil 395 } 396 397 if *delFilter.TypeEquals == "" { 398 delFilter.TypeEquals = nil 399 } 400 401 if *delFilter.IPEquals == "" { 402 delFilter.IPEquals = nil 403 } 404 405 if *delFilter.RangeEquals == "" { 406 delFilter.RangeEquals = nil 407 } 408 if contained != nil && *contained { 409 delFilter.Contains = new(bool) 410 } 411 412 if delDecisionId == "" { 413 decisions, _, err = Client.Decisions.Delete(context.Background(), delFilter) 414 if err != nil { 415 log.Fatalf("Unable to delete decisions : %v", err.Error()) 416 } 417 } else { 418 decisions, _, err = Client.Decisions.DeleteOne(context.Background(), delDecisionId) 419 if err != nil { 420 log.Fatalf("Unable to delete decision : %v", err.Error()) 421 } 422 } 423 log.Infof("%s decision(s) deleted", decisions.NbDeleted) 424 }, 425 } 426 427 cmdDecisionsDelete.Flags().SortFlags = false 428 cmdDecisionsDelete.Flags().StringVarP(delFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)") 429 cmdDecisionsDelete.Flags().StringVarP(delFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)") 430 cmdDecisionsDelete.Flags().StringVar(&delDecisionId, "id", "", "decision id") 431 cmdDecisionsDelete.Flags().StringVarP(delFilter.TypeEquals, "type", "t", "", "the decision type (ie. ban,captcha)") 432 cmdDecisionsDelete.Flags().StringVarP(delFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope") 433 cmdDecisionsDelete.Flags().BoolVar(&delDecisionAll, "all", false, "delete all decisions") 434 cmdDecisionsDelete.Flags().BoolVar(contained, "contained", false, "query decisions contained by range") 435 436 cmdDecisions.AddCommand(cmdDecisionsDelete) 437 438 return cmdDecisions 439 }