alerts.go 18 KB

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