alerts.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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. "text/template"
  13. "github.com/fatih/color"
  14. "github.com/go-openapi/strfmt"
  15. log "github.com/sirupsen/logrus"
  16. "github.com/spf13/cobra"
  17. "gopkg.in/yaml.v2"
  18. "github.com/crowdsecurity/go-cs-lib/version"
  19. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  20. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  21. "github.com/crowdsecurity/crowdsec/pkg/database"
  22. "github.com/crowdsecurity/crowdsec/pkg/models"
  23. "github.com/crowdsecurity/crowdsec/pkg/types"
  24. )
  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. switch csConfig.Cscli.Output {
  46. case "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.GetAsNumberName(),
  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. case "json":
  77. if *alerts == nil {
  78. // avoid returning "null" in json
  79. // could be cleaner if we used slice of alerts directly
  80. fmt.Println("[]")
  81. return nil
  82. }
  83. x, _ := json.MarshalIndent(alerts, "", " ")
  84. fmt.Print(string(x))
  85. case "human":
  86. if len(*alerts) == 0 {
  87. fmt.Println("No active alerts")
  88. return nil
  89. }
  90. alertsTable(color.Output, alerts, printMachine)
  91. }
  92. return nil
  93. }
  94. var alertTemplate = `
  95. ################################################################################################
  96. - ID : {{.ID}}
  97. - Date : {{.CreatedAt}}
  98. - Machine : {{.MachineID}}
  99. - Simulation : {{.Simulated}}
  100. - Reason : {{.Scenario}}
  101. - Events Count : {{.EventsCount}}
  102. - Scope:Value : {{.Source.Scope}}{{if .Source.Value}}:{{.Source.Value}}{{end}}
  103. - Country : {{.Source.Cn}}
  104. - AS : {{.Source.AsName}}
  105. - Begin : {{.StartAt}}
  106. - End : {{.StopAt}}
  107. - UUID : {{.UUID}}
  108. `
  109. func displayOneAlert(alert *models.Alert, withDetail bool) error {
  110. tmpl, err := template.New("alert").Parse(alertTemplate)
  111. if err != nil {
  112. return err
  113. }
  114. err = tmpl.Execute(os.Stdout, alert)
  115. if err != nil {
  116. return err
  117. }
  118. alertDecisionsTable(color.Output, alert)
  119. if len(alert.Meta) > 0 {
  120. fmt.Printf("\n - Context :\n")
  121. sort.Slice(alert.Meta, func(i, j int) bool {
  122. return alert.Meta[i].Key < alert.Meta[j].Key
  123. })
  124. table := newTable(color.Output)
  125. table.SetRowLines(false)
  126. table.SetHeaders("Key", "Value")
  127. for _, meta := range alert.Meta {
  128. var valSlice []string
  129. if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
  130. return fmt.Errorf("unknown context value type '%s' : %s", meta.Value, err)
  131. }
  132. for _, value := range valSlice {
  133. table.AddRow(
  134. meta.Key,
  135. value,
  136. )
  137. }
  138. }
  139. table.Render()
  140. }
  141. if withDetail {
  142. fmt.Printf("\n - Events :\n")
  143. for _, event := range alert.Events {
  144. alertEventTable(color.Output, event)
  145. }
  146. }
  147. return nil
  148. }
  149. type cliAlerts struct{
  150. client *apiclient.ApiClient
  151. }
  152. func NewCLIAlerts() *cliAlerts {
  153. return &cliAlerts{}
  154. }
  155. func (cli *cliAlerts) NewCommand() *cobra.Command {
  156. cmd := &cobra.Command{
  157. Use: "alerts [action]",
  158. Short: "Manage alerts",
  159. Args: cobra.MinimumNArgs(1),
  160. DisableAutoGenTag: true,
  161. Aliases: []string{"alert"},
  162. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  163. var err error
  164. if err := csConfig.LoadAPIClient(); err != nil {
  165. return fmt.Errorf("loading api client: %w", err)
  166. }
  167. apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
  168. if err != nil {
  169. return fmt.Errorf("parsing api url %s: %w", apiURL, err)
  170. }
  171. cli.client, err = apiclient.NewClient(&apiclient.Config{
  172. MachineID: csConfig.API.Client.Credentials.Login,
  173. Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
  174. UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
  175. URL: apiURL,
  176. VersionPrefix: "v1",
  177. })
  178. if err != nil {
  179. return fmt.Errorf("new api client: %w", err)
  180. }
  181. return nil
  182. },
  183. }
  184. cmd.AddCommand(cli.NewListCmd())
  185. cmd.AddCommand(cli.NewInspectCmd())
  186. cmd.AddCommand(cli.NewFlushCmd())
  187. cmd.AddCommand(cli.NewDeleteCmd())
  188. return cmd
  189. }
  190. func (cli *cliAlerts) NewListCmd() *cobra.Command {
  191. var alertListFilter = apiclient.AlertsListOpts{
  192. ScopeEquals: new(string),
  193. ValueEquals: new(string),
  194. ScenarioEquals: new(string),
  195. IPEquals: new(string),
  196. RangeEquals: new(string),
  197. Since: new(string),
  198. Until: new(string),
  199. TypeEquals: new(string),
  200. IncludeCAPI: new(bool),
  201. OriginEquals: new(string),
  202. }
  203. limit := new(int)
  204. contained := new(bool)
  205. var printMachine bool
  206. cmd := &cobra.Command{
  207. Use: "list [filters]",
  208. Short: "List alerts",
  209. Example: `cscli alerts list
  210. cscli alerts list --ip 1.2.3.4
  211. cscli alerts list --range 1.2.3.0/24
  212. cscli alerts list -s crowdsecurity/ssh-bf
  213. cscli alerts list --type ban`,
  214. DisableAutoGenTag: true,
  215. RunE: func(cmd *cobra.Command, args []string) error {
  216. var err error
  217. if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
  218. alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
  219. printHelp(cmd)
  220. return err
  221. }
  222. if limit != nil {
  223. alertListFilter.Limit = limit
  224. }
  225. if *alertListFilter.Until == "" {
  226. alertListFilter.Until = nil
  227. } else if strings.HasSuffix(*alertListFilter.Until, "d") {
  228. /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
  229. realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
  230. days, err := strconv.Atoi(realDuration)
  231. if err != nil {
  232. printHelp(cmd)
  233. return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
  234. }
  235. *alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
  236. }
  237. if *alertListFilter.Since == "" {
  238. alertListFilter.Since = nil
  239. } else if strings.HasSuffix(*alertListFilter.Since, "d") {
  240. /*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
  241. realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
  242. days, err := strconv.Atoi(realDuration)
  243. if err != nil {
  244. printHelp(cmd)
  245. return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
  246. }
  247. *alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
  248. }
  249. if *alertListFilter.IncludeCAPI {
  250. *alertListFilter.Limit = 0
  251. }
  252. if *alertListFilter.TypeEquals == "" {
  253. alertListFilter.TypeEquals = nil
  254. }
  255. if *alertListFilter.ScopeEquals == "" {
  256. alertListFilter.ScopeEquals = nil
  257. }
  258. if *alertListFilter.ValueEquals == "" {
  259. alertListFilter.ValueEquals = nil
  260. }
  261. if *alertListFilter.ScenarioEquals == "" {
  262. alertListFilter.ScenarioEquals = nil
  263. }
  264. if *alertListFilter.IPEquals == "" {
  265. alertListFilter.IPEquals = nil
  266. }
  267. if *alertListFilter.RangeEquals == "" {
  268. alertListFilter.RangeEquals = nil
  269. }
  270. if *alertListFilter.OriginEquals == "" {
  271. alertListFilter.OriginEquals = nil
  272. }
  273. if contained != nil && *contained {
  274. alertListFilter.Contains = new(bool)
  275. }
  276. alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
  277. if err != nil {
  278. return fmt.Errorf("unable to list alerts: %v", err)
  279. }
  280. err = alertsToTable(alerts, printMachine)
  281. if err != nil {
  282. return fmt.Errorf("unable to list alerts: %v", err)
  283. }
  284. return nil
  285. },
  286. }
  287. cmd.Flags().SortFlags = false
  288. cmd.Flags().BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
  289. cmd.Flags().StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
  290. cmd.Flags().StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
  291. cmd.Flags().StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
  292. cmd.Flags().StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
  293. cmd.Flags().StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
  294. cmd.Flags().StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
  295. cmd.Flags().StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
  296. cmd.Flags().StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
  297. cmd.Flags().StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
  298. cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
  299. cmd.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
  300. cmd.Flags().IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
  301. return cmd
  302. }
  303. func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
  304. var ActiveDecision *bool
  305. var AlertDeleteAll bool
  306. var delAlertByID string
  307. contained := new(bool)
  308. var alertDeleteFilter = apiclient.AlertsDeleteOpts{
  309. ScopeEquals: new(string),
  310. ValueEquals: new(string),
  311. ScenarioEquals: new(string),
  312. IPEquals: new(string),
  313. RangeEquals: new(string),
  314. }
  315. cmd := &cobra.Command{
  316. Use: "delete [filters] [--all]",
  317. Short: `Delete alerts
  318. /!\ This command can be use only on the same machine than the local API.`,
  319. Example: `cscli alerts delete --ip 1.2.3.4
  320. cscli alerts delete --range 1.2.3.0/24
  321. cscli alerts delete -s crowdsecurity/ssh-bf"`,
  322. DisableAutoGenTag: true,
  323. Aliases: []string{"remove"},
  324. Args: cobra.ExactArgs(0),
  325. PreRunE: func(cmd *cobra.Command, args []string) error {
  326. if AlertDeleteAll {
  327. return nil
  328. }
  329. if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" &&
  330. *alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
  331. *alertDeleteFilter.RangeEquals == "" && delAlertByID == "" {
  332. _ = cmd.Usage()
  333. return fmt.Errorf("at least one filter or --all must be specified")
  334. }
  335. return nil
  336. },
  337. RunE: func(cmd *cobra.Command, args []string) error {
  338. var err error
  339. if !AlertDeleteAll {
  340. if err := manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
  341. alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
  342. printHelp(cmd)
  343. return err
  344. }
  345. if ActiveDecision != nil {
  346. alertDeleteFilter.ActiveDecisionEquals = ActiveDecision
  347. }
  348. if *alertDeleteFilter.ScopeEquals == "" {
  349. alertDeleteFilter.ScopeEquals = nil
  350. }
  351. if *alertDeleteFilter.ValueEquals == "" {
  352. alertDeleteFilter.ValueEquals = nil
  353. }
  354. if *alertDeleteFilter.ScenarioEquals == "" {
  355. alertDeleteFilter.ScenarioEquals = nil
  356. }
  357. if *alertDeleteFilter.IPEquals == "" {
  358. alertDeleteFilter.IPEquals = nil
  359. }
  360. if *alertDeleteFilter.RangeEquals == "" {
  361. alertDeleteFilter.RangeEquals = nil
  362. }
  363. if contained != nil && *contained {
  364. alertDeleteFilter.Contains = new(bool)
  365. }
  366. limit := 0
  367. alertDeleteFilter.Limit = &limit
  368. } else {
  369. limit := 0
  370. alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit}
  371. }
  372. var alerts *models.DeleteAlertsResponse
  373. if delAlertByID == "" {
  374. alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
  375. if err != nil {
  376. return fmt.Errorf("unable to delete alerts : %v", err)
  377. }
  378. } else {
  379. alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
  380. if err != nil {
  381. return fmt.Errorf("unable to delete alert: %v", err)
  382. }
  383. }
  384. log.Infof("%s alert(s) deleted", alerts.NbDeleted)
  385. return nil
  386. },
  387. }
  388. cmd.Flags().SortFlags = false
  389. cmd.Flags().StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
  390. cmd.Flags().StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
  391. cmd.Flags().StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
  392. cmd.Flags().StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
  393. cmd.Flags().StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
  394. cmd.Flags().StringVar(&delAlertByID, "id", "", "alert ID")
  395. cmd.Flags().BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
  396. cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
  397. return cmd
  398. }
  399. func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
  400. var details bool
  401. cmd := &cobra.Command{
  402. Use: `inspect "alert_id"`,
  403. Short: `Show info about an alert`,
  404. Example: `cscli alerts inspect 123`,
  405. DisableAutoGenTag: true,
  406. RunE: func(cmd *cobra.Command, args []string) error {
  407. if len(args) == 0 {
  408. printHelp(cmd)
  409. return fmt.Errorf("missing alert_id")
  410. }
  411. for _, alertID := range args {
  412. id, err := strconv.Atoi(alertID)
  413. if err != nil {
  414. return fmt.Errorf("bad alert id %s", alertID)
  415. }
  416. alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
  417. if err != nil {
  418. return fmt.Errorf("can't find alert with id %s: %s", alertID, err)
  419. }
  420. switch csConfig.Cscli.Output {
  421. case "human":
  422. if err := displayOneAlert(alert, details); err != nil {
  423. continue
  424. }
  425. case "json":
  426. data, err := json.MarshalIndent(alert, "", " ")
  427. if err != nil {
  428. return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
  429. }
  430. fmt.Printf("%s\n", string(data))
  431. case "raw":
  432. data, err := yaml.Marshal(alert)
  433. if err != nil {
  434. return fmt.Errorf("unable to marshal alert with id %s: %s", alertID, err)
  435. }
  436. fmt.Printf("%s\n", string(data))
  437. }
  438. }
  439. return nil
  440. },
  441. }
  442. cmd.Flags().SortFlags = false
  443. cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
  444. return cmd
  445. }
  446. func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
  447. var maxItems int
  448. var maxAge string
  449. cmd := &cobra.Command{
  450. Use: `flush`,
  451. Short: `Flush alerts
  452. /!\ This command can be used only on the same machine than the local API`,
  453. Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
  454. DisableAutoGenTag: true,
  455. RunE: func(cmd *cobra.Command, args []string) error {
  456. var err error
  457. if err := require.LAPI(csConfig); err != nil {
  458. return err
  459. }
  460. db, err := database.NewClient(csConfig.DbConfig)
  461. if err != nil {
  462. return fmt.Errorf("unable to create new database client: %s", err)
  463. }
  464. log.Info("Flushing alerts. !! This may take a long time !!")
  465. err = db.FlushAlerts(maxAge, maxItems)
  466. if err != nil {
  467. return fmt.Errorf("unable to flush alerts: %s", err)
  468. }
  469. log.Info("Alerts flushed")
  470. return nil
  471. },
  472. }
  473. cmd.Flags().SortFlags = false
  474. cmd.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
  475. cmd.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")
  476. return cmd
  477. }