alerts.go 17 KB

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