alerts.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. package main
  2. import (
  3. "context"
  4. "encoding/csv"
  5. "encoding/json"
  6. "fmt"
  7. "net/url"
  8. "os"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  14. "github.com/crowdsecurity/crowdsec/pkg/cwversion"
  15. "github.com/crowdsecurity/crowdsec/pkg/database"
  16. "github.com/crowdsecurity/crowdsec/pkg/models"
  17. "github.com/go-openapi/strfmt"
  18. "github.com/olekukonko/tablewriter"
  19. "github.com/pkg/errors"
  20. log "github.com/sirupsen/logrus"
  21. "github.com/spf13/cobra"
  22. "gopkg.in/yaml.v2"
  23. )
  24. var printMachine bool
  25. var limit *int
  26. func DecisionsFromAlert(alert *models.Alert) string {
  27. ret := ""
  28. var decMap = make(map[string]int)
  29. for _, decision := range alert.Decisions {
  30. k := *decision.Type
  31. if *decision.Simulated {
  32. k = fmt.Sprintf("(simul)%s", k)
  33. }
  34. v := decMap[k]
  35. decMap[k] = v + 1
  36. }
  37. for k, v := range decMap {
  38. if len(ret) > 0 {
  39. ret += " "
  40. }
  41. ret += fmt.Sprintf("%s:%d", k, v)
  42. }
  43. return ret
  44. }
  45. func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
  46. if csConfig.Cscli.Output == "raw" {
  47. csvwriter := csv.NewWriter(os.Stdout)
  48. header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
  49. if printMachine {
  50. header = append(header, "machine")
  51. }
  52. err := csvwriter.Write(header)
  53. if err != nil {
  54. return err
  55. }
  56. for _, alertItem := range *alerts {
  57. row := []string{
  58. fmt.Sprintf("%d", alertItem.ID),
  59. *alertItem.Source.Scope,
  60. *alertItem.Source.Value,
  61. *alertItem.Scenario,
  62. alertItem.Source.Cn,
  63. alertItem.Source.AsNumber + " " + alertItem.Source.AsName,
  64. DecisionsFromAlert(alertItem),
  65. *alertItem.StartAt,
  66. }
  67. if printMachine {
  68. row = append(row, alertItem.MachineID)
  69. }
  70. err := csvwriter.Write(row)
  71. if err != nil {
  72. return err
  73. }
  74. }
  75. csvwriter.Flush()
  76. } else if csConfig.Cscli.Output == "json" {
  77. x, _ := json.MarshalIndent(alerts, "", " ")
  78. fmt.Printf("%s", string(x))
  79. } else if csConfig.Cscli.Output == "human" {
  80. table := tablewriter.NewWriter(os.Stdout)
  81. header := []string{"ID", "value", "reason", "country", "as", "decisions", "created_at"}
  82. if printMachine {
  83. header = append(header, "machine")
  84. }
  85. table.SetHeader(header)
  86. if len(*alerts) == 0 {
  87. fmt.Println("No active alerts")
  88. return nil
  89. }
  90. for _, alertItem := range *alerts {
  91. displayVal := *alertItem.Source.Scope
  92. if *alertItem.Source.Value != "" {
  93. displayVal += ":" + *alertItem.Source.Value
  94. }
  95. row := []string{
  96. strconv.Itoa(int(alertItem.ID)),
  97. displayVal,
  98. *alertItem.Scenario,
  99. alertItem.Source.Cn,
  100. alertItem.Source.AsNumber + " " + alertItem.Source.AsName,
  101. DecisionsFromAlert(alertItem),
  102. *alertItem.StartAt,
  103. }
  104. if printMachine {
  105. row = append(row, alertItem.MachineID)
  106. }
  107. table.Append(row)
  108. }
  109. table.Render() // Send output
  110. }
  111. return nil
  112. }
  113. func DisplayOneAlert(alert *models.Alert, withDetail bool) error {
  114. if csConfig.Cscli.Output == "human" {
  115. fmt.Printf("\n################################################################################################\n\n")
  116. scopeAndValue := *alert.Source.Scope
  117. if *alert.Source.Value != "" {
  118. scopeAndValue += ":" + *alert.Source.Value
  119. }
  120. fmt.Printf(" - ID : %d\n", alert.ID)
  121. fmt.Printf(" - Date : %s\n", alert.CreatedAt)
  122. fmt.Printf(" - Machine : %s\n", alert.MachineID)
  123. fmt.Printf(" - Simulation : %v\n", *alert.Simulated)
  124. fmt.Printf(" - Reason : %s\n", *alert.Scenario)
  125. fmt.Printf(" - Events Count : %d\n", *alert.EventsCount)
  126. fmt.Printf(" - Scope:Value: %s\n", scopeAndValue)
  127. fmt.Printf(" - Country : %s\n", alert.Source.Cn)
  128. fmt.Printf(" - AS : %s\n", alert.Source.AsName)
  129. fmt.Printf(" - Begin : %s\n", *alert.StartAt)
  130. fmt.Printf(" - End : %s\n\n", *alert.StopAt)
  131. foundActive := false
  132. table := tablewriter.NewWriter(os.Stdout)
  133. table.SetHeader([]string{"ID", "scope:value", "action", "expiration", "created_at"})
  134. for _, decision := range alert.Decisions {
  135. parsedDuration, err := time.ParseDuration(*decision.Duration)
  136. if err != nil {
  137. log.Errorf(err.Error())
  138. }
  139. expire := time.Now().UTC().Add(parsedDuration)
  140. if time.Now().UTC().After(expire) {
  141. continue
  142. }
  143. foundActive = true
  144. scopeAndValue := *decision.Scope
  145. if *decision.Value != "" {
  146. scopeAndValue += ":" + *decision.Value
  147. }
  148. table.Append([]string{
  149. strconv.Itoa(int(decision.ID)),
  150. scopeAndValue,
  151. *decision.Type,
  152. *decision.Duration,
  153. alert.CreatedAt,
  154. })
  155. }
  156. if foundActive {
  157. fmt.Printf(" - Active Decisions :\n")
  158. table.Render() // Send output
  159. }
  160. if withDetail {
  161. fmt.Printf("\n - Events :\n")
  162. for _, event := range alert.Events {
  163. fmt.Printf("\n- Date: %s\n", *event.Timestamp)
  164. table = tablewriter.NewWriter(os.Stdout)
  165. table.SetHeader([]string{"Key", "Value"})
  166. sort.Slice(event.Meta, func(i, j int) bool {
  167. return event.Meta[i].Key < event.Meta[j].Key
  168. })
  169. for _, meta := range event.Meta {
  170. table.Append([]string{
  171. meta.Key,
  172. meta.Value,
  173. })
  174. }
  175. table.Render() // Send output
  176. }
  177. }
  178. }
  179. return nil
  180. }
  181. func NewAlertsCmd() *cobra.Command {
  182. /* ---- ALERTS COMMAND */
  183. var cmdAlerts = &cobra.Command{
  184. Use: "alerts [action]",
  185. Short: "Manage alerts",
  186. Args: cobra.MinimumNArgs(1),
  187. DisableAutoGenTag: true,
  188. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  189. var err error
  190. if err := csConfig.LoadAPIClient(); err != nil {
  191. return errors.Wrap(err, "loading api client")
  192. }
  193. apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
  194. if err != nil {
  195. return errors.Wrapf(err, "parsing api url %s", apiURL)
  196. }
  197. Client, err = apiclient.NewClient(&apiclient.Config{
  198. MachineID: csConfig.API.Client.Credentials.Login,
  199. Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
  200. UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  201. URL: apiURL,
  202. VersionPrefix: "v1",
  203. })
  204. if err != nil {
  205. return errors.Wrap(err, "new api client")
  206. }
  207. return nil
  208. },
  209. }
  210. var alertListFilter = apiclient.AlertsListOpts{
  211. ScopeEquals: new(string),
  212. ValueEquals: new(string),
  213. ScenarioEquals: new(string),
  214. IPEquals: new(string),
  215. RangeEquals: new(string),
  216. Since: new(string),
  217. Until: new(string),
  218. TypeEquals: new(string),
  219. }
  220. limit = new(int)
  221. contained := new(bool)
  222. var cmdAlertsList = &cobra.Command{
  223. Use: "list [filters]",
  224. Short: "List alerts",
  225. Example: `cscli alerts list
  226. cscli alerts list --ip 1.2.3.4
  227. cscli alerts list --range 1.2.3.0/24
  228. cscli alerts list -s crowdsecurity/ssh-bf
  229. cscli alerts list --type ban`,
  230. DisableAutoGenTag: true,
  231. Run: func(cmd *cobra.Command, args []string) {
  232. var err error
  233. if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
  234. alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
  235. printHelp(cmd)
  236. log.Fatalf("%s", err)
  237. }
  238. if limit != nil {
  239. alertListFilter.Limit = limit
  240. }
  241. if *alertListFilter.Until == "" {
  242. alertListFilter.Until = nil
  243. } else {
  244. /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
  245. if strings.HasSuffix(*alertListFilter.Until, "d") {
  246. realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
  247. days, err := strconv.Atoi(realDuration)
  248. if err != nil {
  249. printHelp(cmd)
  250. log.Fatalf("Can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
  251. }
  252. *alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
  253. }
  254. }
  255. if *alertListFilter.Since == "" {
  256. alertListFilter.Since = nil
  257. } else {
  258. /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
  259. if strings.HasSuffix(*alertListFilter.Since, "d") {
  260. realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
  261. days, err := strconv.Atoi(realDuration)
  262. if err != nil {
  263. printHelp(cmd)
  264. log.Fatalf("Can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
  265. }
  266. *alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
  267. }
  268. }
  269. if *alertListFilter.TypeEquals == "" {
  270. alertListFilter.TypeEquals = nil
  271. }
  272. if *alertListFilter.ScopeEquals == "" {
  273. alertListFilter.ScopeEquals = nil
  274. }
  275. if *alertListFilter.ValueEquals == "" {
  276. alertListFilter.ValueEquals = nil
  277. }
  278. if *alertListFilter.ScenarioEquals == "" {
  279. alertListFilter.ScenarioEquals = nil
  280. }
  281. if *alertListFilter.IPEquals == "" {
  282. alertListFilter.IPEquals = nil
  283. }
  284. if *alertListFilter.RangeEquals == "" {
  285. alertListFilter.RangeEquals = nil
  286. }
  287. if contained != nil && *contained {
  288. alertListFilter.Contains = new(bool)
  289. }
  290. alerts, _, err := Client.Alerts.List(context.Background(), alertListFilter)
  291. if err != nil {
  292. log.Fatalf("Unable to list alerts : %v", err.Error())
  293. }
  294. err = AlertsToTable(alerts, printMachine)
  295. if err != nil {
  296. log.Fatalf("unable to list alerts : %v", err.Error())
  297. }
  298. },
  299. }
  300. cmdAlertsList.Flags().SortFlags = false
  301. cmdAlertsList.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
  302. cmdAlertsList.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
  303. cmdAlertsList.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
  304. cmdAlertsList.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
  305. cmdAlertsList.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
  306. cmdAlertsList.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
  307. cmdAlertsList.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
  308. cmdAlertsList.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
  309. cmdAlertsList.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
  310. cmdAlertsList.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
  311. cmdAlertsList.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
  312. cmdAlerts.AddCommand(cmdAlertsList)
  313. var ActiveDecision *bool
  314. var AlertDeleteAll bool
  315. var alertDeleteFilter = apiclient.AlertsDeleteOpts{
  316. ScopeEquals: new(string),
  317. ValueEquals: new(string),
  318. ScenarioEquals: new(string),
  319. IPEquals: new(string),
  320. RangeEquals: new(string),
  321. }
  322. var cmdAlertsDelete = &cobra.Command{
  323. Use: "delete [filters] [--all]",
  324. Short: `Delete alerts
  325. /!\ This command can be use only on the same machine than the local API.`,
  326. Example: `cscli alerts delete --ip 1.2.3.4
  327. cscli alerts delete --range 1.2.3.0/24
  328. cscli alerts delete -s crowdsecurity/ssh-bf"`,
  329. DisableAutoGenTag: true,
  330. Aliases: []string{"remove"},
  331. Args: cobra.ExactArgs(0),
  332. PreRun: func(cmd *cobra.Command, args []string) {
  333. if AlertDeleteAll {
  334. return
  335. }
  336. if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" &&
  337. *alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
  338. *alertDeleteFilter.RangeEquals == "" {
  339. _ = cmd.Usage()
  340. log.Fatalln("At least one filter or --all must be specified")
  341. }
  342. },
  343. Run: func(cmd *cobra.Command, args []string) {
  344. var err error
  345. if !AlertDeleteAll {
  346. if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
  347. alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
  348. printHelp(cmd)
  349. log.Fatalf("%s", err)
  350. }
  351. if ActiveDecision != nil {
  352. alertDeleteFilter.ActiveDecisionEquals = ActiveDecision
  353. }
  354. if *alertDeleteFilter.ScopeEquals == "" {
  355. alertDeleteFilter.ScopeEquals = nil
  356. }
  357. if *alertDeleteFilter.ValueEquals == "" {
  358. alertDeleteFilter.ValueEquals = nil
  359. }
  360. if *alertDeleteFilter.ScenarioEquals == "" {
  361. alertDeleteFilter.ScenarioEquals = nil
  362. }
  363. if *alertDeleteFilter.IPEquals == "" {
  364. alertDeleteFilter.IPEquals = nil
  365. }
  366. if *alertDeleteFilter.RangeEquals == "" {
  367. alertDeleteFilter.RangeEquals = nil
  368. }
  369. if contained != nil && *contained {
  370. alertDeleteFilter.Contains = new(bool)
  371. }
  372. } else {
  373. limit := 0
  374. alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit}
  375. }
  376. alerts, _, err := Client.Alerts.Delete(context.Background(), alertDeleteFilter)
  377. if err != nil {
  378. log.Fatalf("Unable to delete alerts : %v", err.Error())
  379. }
  380. log.Infof("%s alert(s) deleted", alerts.NbDeleted)
  381. },
  382. }
  383. cmdAlertsDelete.Flags().SortFlags = false
  384. cmdAlertsDelete.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
  385. cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
  386. cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
  387. cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
  388. cmdAlertsDelete.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
  389. cmdAlertsDelete.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
  390. cmdAlertsDelete.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
  391. cmdAlerts.AddCommand(cmdAlertsDelete)
  392. var details bool
  393. var cmdAlertsInspect = &cobra.Command{
  394. Use: `inspect "alert_id"`,
  395. Short: `Show info about an alert`,
  396. Example: `cscli alerts inspect 123`,
  397. DisableAutoGenTag: true,
  398. Run: func(cmd *cobra.Command, args []string) {
  399. if len(args) == 0 {
  400. printHelp(cmd)
  401. return
  402. }
  403. for _, alertID := range args {
  404. id, err := strconv.Atoi(alertID)
  405. if err != nil {
  406. log.Fatalf("bad alert id %s", alertID)
  407. continue
  408. }
  409. alert, _, err := Client.Alerts.GetByID(context.Background(), id)
  410. if err != nil {
  411. log.Fatalf("can't find alert with id %s: %s", alertID, err)
  412. }
  413. switch csConfig.Cscli.Output {
  414. case "human":
  415. if err := DisplayOneAlert(alert, details); err != nil {
  416. continue
  417. }
  418. case "json":
  419. data, err := json.MarshalIndent(alert, "", " ")
  420. if err != nil {
  421. log.Fatalf("unable to marshal alert with id %s: %s", alertID, err)
  422. }
  423. fmt.Printf("%s\n", string(data))
  424. case "raw":
  425. data, err := yaml.Marshal(alert)
  426. if err != nil {
  427. log.Fatalf("unable to marshal alert with id %s: %s", alertID, err)
  428. }
  429. fmt.Printf("%s\n", string(data))
  430. }
  431. }
  432. },
  433. }
  434. cmdAlertsInspect.Flags().SortFlags = false
  435. cmdAlertsInspect.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
  436. cmdAlerts.AddCommand(cmdAlertsInspect)
  437. var maxItems int
  438. var maxAge string
  439. var cmdAlertsFlush = &cobra.Command{
  440. Use: `flush`,
  441. Short: `Flush alerts
  442. /!\ This command can be used only on the same machine than the local API`,
  443. Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
  444. DisableAutoGenTag: true,
  445. Run: func(cmd *cobra.Command, args []string) {
  446. var err error
  447. if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
  448. log.Fatal("Local API is disabled, please run this command on the local API machine")
  449. }
  450. if err := csConfig.LoadDBConfig(); err != nil {
  451. log.Fatalf(err.Error())
  452. }
  453. dbClient, err = database.NewClient(csConfig.DbConfig)
  454. if err != nil {
  455. log.Fatalf("unable to create new database client: %s", err)
  456. }
  457. log.Info("Flushing alerts. !! This may take a long time !!")
  458. err = dbClient.FlushAlerts(maxAge, maxItems)
  459. if err != nil {
  460. log.Fatalf("unable to flush alerts: %s", err)
  461. }
  462. log.Info("Alerts flushed")
  463. },
  464. }
  465. cmdAlertsFlush.Flags().SortFlags = false
  466. cmdAlertsFlush.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
  467. cmdAlertsFlush.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")
  468. cmdAlerts.AddCommand(cmdAlertsFlush)
  469. return cmdAlerts
  470. }