notifications.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. package main
  2. import (
  3. "context"
  4. "encoding/csv"
  5. "encoding/json"
  6. "fmt"
  7. "io/fs"
  8. "net/url"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  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/tomb.v2"
  19. "gopkg.in/yaml.v3"
  20. "github.com/crowdsecurity/go-cs-lib/ptr"
  21. "github.com/crowdsecurity/go-cs-lib/version"
  22. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  23. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  24. "github.com/crowdsecurity/crowdsec/pkg/csplugin"
  25. "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
  26. "github.com/crowdsecurity/crowdsec/pkg/types"
  27. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  28. "github.com/crowdsecurity/crowdsec/pkg/models"
  29. )
  30. type NotificationsCfg struct {
  31. Config csplugin.PluginConfig `json:"plugin_config"`
  32. Profiles []*csconfig.ProfileCfg `json:"associated_profiles"`
  33. ids []uint
  34. }
  35. type cliNotifications struct {
  36. cfg configGetter
  37. }
  38. func NewCLINotifications(cfg configGetter) *cliNotifications {
  39. return &cliNotifications{
  40. cfg: cfg,
  41. }
  42. }
  43. func (cli *cliNotifications) NewCommand() *cobra.Command {
  44. cmd := &cobra.Command{
  45. Use: "notifications [action]",
  46. Short: "Helper for notification plugin configuration",
  47. Long: "To list/inspect/test notification template",
  48. Args: cobra.MinimumNArgs(1),
  49. Aliases: []string{"notifications", "notification"},
  50. DisableAutoGenTag: true,
  51. PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
  52. cfg := cli.cfg()
  53. if err := require.LAPI(cfg); err != nil {
  54. return err
  55. }
  56. if err := cfg.LoadAPIClient(); err != nil {
  57. return fmt.Errorf("loading api client: %w", err)
  58. }
  59. if err := require.Notifications(cfg); err != nil {
  60. return err
  61. }
  62. return nil
  63. },
  64. }
  65. cmd.AddCommand(cli.NewListCmd())
  66. cmd.AddCommand(cli.NewInspectCmd())
  67. cmd.AddCommand(cli.NewReinjectCmd())
  68. cmd.AddCommand(cli.NewTestCmd())
  69. return cmd
  70. }
  71. func (cli *cliNotifications) getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
  72. cfg := cli.cfg()
  73. pcfgs := map[string]csplugin.PluginConfig{}
  74. wf := func(path string, info fs.FileInfo, err error) error {
  75. if info == nil {
  76. return fmt.Errorf("error while traversing directory %s: %w", path, err)
  77. }
  78. name := filepath.Join(cfg.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
  79. if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
  80. ts, err := csplugin.ParsePluginConfigFile(name)
  81. if err != nil {
  82. return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
  83. }
  84. for _, t := range ts {
  85. csplugin.SetRequiredFields(&t)
  86. pcfgs[t.Name] = t
  87. }
  88. }
  89. return nil
  90. }
  91. if err := filepath.Walk(cfg.ConfigPaths.NotificationDir, wf); err != nil {
  92. return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
  93. }
  94. return pcfgs, nil
  95. }
  96. func (cli *cliNotifications) getProfilesConfigs() (map[string]NotificationsCfg, error) {
  97. cfg := cli.cfg()
  98. // A bit of a tricky stuf now: reconcile profiles and notification plugins
  99. pcfgs, err := cli.getPluginConfigs()
  100. if err != nil {
  101. return nil, err
  102. }
  103. ncfgs := map[string]NotificationsCfg{}
  104. for _, pc := range pcfgs {
  105. ncfgs[pc.Name] = NotificationsCfg{
  106. Config: pc,
  107. }
  108. }
  109. profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
  110. if err != nil {
  111. return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
  112. }
  113. for profileID, profile := range profiles {
  114. for _, notif := range profile.Cfg.Notifications {
  115. pc, ok := pcfgs[notif]
  116. if !ok {
  117. return nil, fmt.Errorf("notification plugin '%s' does not exist", notif)
  118. }
  119. tmp, ok := ncfgs[pc.Name]
  120. if !ok {
  121. return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name)
  122. }
  123. tmp.Profiles = append(tmp.Profiles, profile.Cfg)
  124. tmp.ids = append(tmp.ids, uint(profileID))
  125. ncfgs[pc.Name] = tmp
  126. }
  127. }
  128. return ncfgs, nil
  129. }
  130. func (cli *cliNotifications) NewListCmd() *cobra.Command {
  131. cmd := &cobra.Command{
  132. Use: "list",
  133. Short: "list active notifications plugins",
  134. Long: `list active notifications plugins`,
  135. Example: `cscli notifications list`,
  136. Args: cobra.ExactArgs(0),
  137. DisableAutoGenTag: true,
  138. RunE: func(_ *cobra.Command, _ []string) error {
  139. cfg := cli.cfg()
  140. ncfgs, err := cli.getProfilesConfigs()
  141. if err != nil {
  142. return fmt.Errorf("can't build profiles configuration: %w", err)
  143. }
  144. if cfg.Cscli.Output == "human" {
  145. notificationListTable(color.Output, ncfgs)
  146. } else if cfg.Cscli.Output == "json" {
  147. x, err := json.MarshalIndent(ncfgs, "", " ")
  148. if err != nil {
  149. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  150. }
  151. fmt.Printf("%s", string(x))
  152. } else if cfg.Cscli.Output == "raw" {
  153. csvwriter := csv.NewWriter(os.Stdout)
  154. err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
  155. if err != nil {
  156. return fmt.Errorf("failed to write raw header: %w", err)
  157. }
  158. for _, b := range ncfgs {
  159. profilesList := []string{}
  160. for _, p := range b.Profiles {
  161. profilesList = append(profilesList, p.Name)
  162. }
  163. err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")})
  164. if err != nil {
  165. return fmt.Errorf("failed to write raw content: %w", err)
  166. }
  167. }
  168. csvwriter.Flush()
  169. }
  170. return nil
  171. },
  172. }
  173. return cmd
  174. }
  175. func (cli *cliNotifications) NewInspectCmd() *cobra.Command {
  176. cmd := &cobra.Command{
  177. Use: "inspect",
  178. Short: "Inspect active notifications plugin configuration",
  179. Long: `Inspect active notifications plugin and show configuration`,
  180. Example: `cscli notifications inspect <plugin_name>`,
  181. Args: cobra.ExactArgs(1),
  182. DisableAutoGenTag: true,
  183. RunE: func(_ *cobra.Command, args []string) error {
  184. cfg := cli.cfg()
  185. ncfgs, err := cli.getProfilesConfigs()
  186. if err != nil {
  187. return fmt.Errorf("can't build profiles configuration: %w", err)
  188. }
  189. ncfg, ok := ncfgs[args[0]]
  190. if !ok {
  191. return fmt.Errorf("plugin '%s' does not exist or is not active", args[0])
  192. }
  193. if cfg.Cscli.Output == "human" || cfg.Cscli.Output == "raw" {
  194. fmt.Printf(" - %15s: %15s\n", "Type", ncfg.Config.Type)
  195. fmt.Printf(" - %15s: %15s\n", "Name", ncfg.Config.Name)
  196. fmt.Printf(" - %15s: %15s\n", "Timeout", ncfg.Config.TimeOut)
  197. fmt.Printf(" - %15s: %15s\n", "Format", ncfg.Config.Format)
  198. for k, v := range ncfg.Config.Config {
  199. fmt.Printf(" - %15s: %15v\n", k, v)
  200. }
  201. } else if cfg.Cscli.Output == "json" {
  202. x, err := json.MarshalIndent(cfg, "", " ")
  203. if err != nil {
  204. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  205. }
  206. fmt.Printf("%s", string(x))
  207. }
  208. return nil
  209. },
  210. }
  211. return cmd
  212. }
  213. func (cli *cliNotifications) NewTestCmd() *cobra.Command {
  214. var (
  215. pluginBroker csplugin.PluginBroker
  216. pluginTomb tomb.Tomb
  217. alertOverride string
  218. )
  219. cmd := &cobra.Command{
  220. Use: "test [plugin name]",
  221. Short: "send a generic test alert to notification plugin",
  222. Long: `send a generic test alert to a notification plugin to test configuration even if is not active`,
  223. Example: `cscli notifications test [plugin_name]`,
  224. Args: cobra.ExactArgs(1),
  225. DisableAutoGenTag: true,
  226. PreRunE: func(_ *cobra.Command, args []string) error {
  227. cfg := cli.cfg()
  228. pconfigs, err := cli.getPluginConfigs()
  229. if err != nil {
  230. return fmt.Errorf("can't build profiles configuration: %w", err)
  231. }
  232. pcfg, ok := pconfigs[args[0]]
  233. if !ok {
  234. return fmt.Errorf("plugin name: '%s' does not exist", args[0])
  235. }
  236. //Create a single profile with plugin name as notification name
  237. return pluginBroker.Init(cfg.PluginConfig, []*csconfig.ProfileCfg{
  238. {
  239. Notifications: []string{
  240. pcfg.Name,
  241. },
  242. },
  243. }, cfg.ConfigPaths)
  244. },
  245. RunE: func(_ *cobra.Command, _ []string) error {
  246. pluginTomb.Go(func() error {
  247. pluginBroker.Run(&pluginTomb)
  248. return nil
  249. })
  250. alert := &models.Alert{
  251. Capacity: ptr.Of(int32(0)),
  252. Decisions: []*models.Decision{{
  253. Duration: ptr.Of("4h"),
  254. Scope: ptr.Of("Ip"),
  255. Value: ptr.Of("10.10.10.10"),
  256. Type: ptr.Of("ban"),
  257. Scenario: ptr.Of("test alert"),
  258. Origin: ptr.Of(types.CscliOrigin),
  259. }},
  260. Events: []*models.Event{},
  261. EventsCount: ptr.Of(int32(1)),
  262. Leakspeed: ptr.Of("0"),
  263. Message: ptr.Of("test alert"),
  264. ScenarioHash: ptr.Of(""),
  265. Scenario: ptr.Of("test alert"),
  266. ScenarioVersion: ptr.Of(""),
  267. Simulated: ptr.Of(false),
  268. Source: &models.Source{
  269. AsName: "",
  270. AsNumber: "",
  271. Cn: "",
  272. IP: "10.10.10.10",
  273. Range: "",
  274. Scope: ptr.Of("Ip"),
  275. Value: ptr.Of("10.10.10.10"),
  276. },
  277. StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  278. StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  279. CreatedAt: time.Now().UTC().Format(time.RFC3339),
  280. }
  281. if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil {
  282. return fmt.Errorf("failed to unmarshal alert override: %w", err)
  283. }
  284. pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  285. ProfileID: uint(0),
  286. Alert: alert,
  287. }
  288. //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  289. pluginTomb.Kill(fmt.Errorf("terminating"))
  290. pluginTomb.Wait()
  291. return nil
  292. },
  293. }
  294. cmd.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the generic alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)")
  295. return cmd
  296. }
  297. func (cli *cliNotifications) NewReinjectCmd() *cobra.Command {
  298. var alertOverride string
  299. var alert *models.Alert
  300. cmd := &cobra.Command{
  301. Use: "reinject",
  302. Short: "reinject an alert into profiles to trigger notifications",
  303. Long: `reinject an alert into profiles to be evaluated by the filter and sent to matched notifications plugins`,
  304. Example: `
  305. cscli notifications reinject <alert_id>
  306. cscli notifications reinject <alert_id> -a '{"remediation": false,"scenario":"notification/test"}'
  307. cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}'
  308. `,
  309. Args: cobra.ExactArgs(1),
  310. DisableAutoGenTag: true,
  311. PreRunE: func(_ *cobra.Command, args []string) error {
  312. var err error
  313. alert, err = cli.fetchAlertFromArgString(args[0])
  314. if err != nil {
  315. return err
  316. }
  317. return nil
  318. },
  319. RunE: func(_ *cobra.Command, _ []string) error {
  320. var (
  321. pluginBroker csplugin.PluginBroker
  322. pluginTomb tomb.Tomb
  323. )
  324. cfg := cli.cfg()
  325. if alertOverride != "" {
  326. if err := json.Unmarshal([]byte(alertOverride), alert); err != nil {
  327. return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
  328. }
  329. }
  330. err := pluginBroker.Init(cfg.PluginConfig, cfg.API.Server.Profiles, cfg.ConfigPaths)
  331. if err != nil {
  332. return fmt.Errorf("can't initialize plugins: %w", err)
  333. }
  334. pluginTomb.Go(func() error {
  335. pluginBroker.Run(&pluginTomb)
  336. return nil
  337. })
  338. profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
  339. if err != nil {
  340. return fmt.Errorf("cannot extract profiles from configuration: %w", err)
  341. }
  342. for id, profile := range profiles {
  343. _, matched, err := profile.EvaluateProfile(alert)
  344. if err != nil {
  345. return fmt.Errorf("can't evaluate profile %s: %w", profile.Cfg.Name, err)
  346. }
  347. if !matched {
  348. log.Infof("The profile %s didn't match", profile.Cfg.Name)
  349. continue
  350. }
  351. log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name)
  352. loop:
  353. for {
  354. select {
  355. case pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  356. ProfileID: uint(id),
  357. Alert: alert,
  358. }:
  359. break loop
  360. default:
  361. time.Sleep(50 * time.Millisecond)
  362. log.Info("sleeping\n")
  363. }
  364. }
  365. if profile.Cfg.OnSuccess == "break" {
  366. log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
  367. break
  368. }
  369. }
  370. //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  371. pluginTomb.Kill(fmt.Errorf("terminating"))
  372. pluginTomb.Wait()
  373. return nil
  374. },
  375. }
  376. cmd.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the reinjected alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)")
  377. return cmd
  378. }
  379. func (cli *cliNotifications) fetchAlertFromArgString(toParse string) (*models.Alert, error) {
  380. cfg := cli.cfg()
  381. id, err := strconv.Atoi(toParse)
  382. if err != nil {
  383. return nil, fmt.Errorf("bad alert id %s", toParse)
  384. }
  385. apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
  386. if err != nil {
  387. return nil, fmt.Errorf("error parsing the URL of the API: %w", err)
  388. }
  389. client, err := apiclient.NewClient(&apiclient.Config{
  390. MachineID: cfg.API.Client.Credentials.Login,
  391. Password: strfmt.Password(cfg.API.Client.Credentials.Password),
  392. UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
  393. URL: apiURL,
  394. VersionPrefix: "v1",
  395. })
  396. if err != nil {
  397. return nil, fmt.Errorf("error creating the client for the API: %w", err)
  398. }
  399. alert, _, err := client.Alerts.GetByID(context.Background(), id)
  400. if err != nil {
  401. return nil, fmt.Errorf("can't find alert with id %d: %w", id, err)
  402. }
  403. return alert, nil
  404. }