decisions.go 16 KB

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