decisions.go 15 KB

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