notifications.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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/go-openapi/strfmt"
  15. colorable "github.com/mattn/go-colorable"
  16. "github.com/pkg/errors"
  17. log "github.com/sirupsen/logrus"
  18. "github.com/spf13/cobra"
  19. "gopkg.in/tomb.v2"
  20. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  21. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  22. "github.com/crowdsecurity/crowdsec/pkg/csplugin"
  23. "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
  24. "github.com/crowdsecurity/crowdsec/pkg/cwversion"
  25. )
  26. type NotificationsCfg struct {
  27. Config csplugin.PluginConfig `json:"plugin_config"`
  28. Profiles []*csconfig.ProfileCfg `json:"associated_profiles"`
  29. ids []uint
  30. }
  31. func NewNotificationsCmd() *cobra.Command {
  32. var cmdNotifications = &cobra.Command{
  33. Use: "notifications [action]",
  34. Short: "Helper for notification plugin configuration",
  35. Long: "To list/inspect/test notification template",
  36. Args: cobra.MinimumNArgs(1),
  37. Aliases: []string{"notifications", "notification"},
  38. DisableAutoGenTag: true,
  39. PersistentPreRun: func(cmd *cobra.Command, args []string) {
  40. var (
  41. err error
  42. )
  43. if err = csConfig.API.Server.LoadProfiles(); err != nil {
  44. log.Fatalf(err.Error())
  45. }
  46. if csConfig.ConfigPaths.NotificationDir == "" {
  47. log.Fatalf("config_paths.notification_dir is not set in crowdsec config")
  48. }
  49. },
  50. }
  51. var cmdNotificationsList = &cobra.Command{
  52. Use: "list",
  53. Short: "List active notifications plugins",
  54. Long: `List active notifications plugins`,
  55. Example: `cscli notifications list`,
  56. Args: cobra.ExactArgs(0),
  57. DisableAutoGenTag: true,
  58. RunE: func(cmd *cobra.Command, arg []string) error {
  59. ncfgs, err := getNotificationsConfiguration()
  60. if err != nil {
  61. return errors.Wrap(err, "Can't build profiles configuration")
  62. }
  63. if csConfig.Cscli.Output == "human" {
  64. notificationListTable(colorable.NewColorableStdout(), ncfgs)
  65. } else if csConfig.Cscli.Output == "json" {
  66. x, err := json.MarshalIndent(ncfgs, "", " ")
  67. if err != nil {
  68. return errors.New("failed to marshal notification configuration")
  69. }
  70. fmt.Printf("%s", string(x))
  71. } else if csConfig.Cscli.Output == "raw" {
  72. csvwriter := csv.NewWriter(os.Stdout)
  73. err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
  74. if err != nil {
  75. return errors.Wrap(err, "failed to write raw header")
  76. }
  77. for _, b := range ncfgs {
  78. profilesList := []string{}
  79. for _, p := range b.Profiles {
  80. profilesList = append(profilesList, p.Name)
  81. }
  82. err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")})
  83. if err != nil {
  84. return errors.Wrap(err, "failed to write raw content")
  85. }
  86. }
  87. csvwriter.Flush()
  88. }
  89. return nil
  90. },
  91. }
  92. cmdNotifications.AddCommand(cmdNotificationsList)
  93. var cmdNotificationsInspect = &cobra.Command{
  94. Use: "inspect",
  95. Short: "Inspect active notifications plugin configuration",
  96. Long: `Inspect active notifications plugin and show configuration`,
  97. Example: `cscli notifications inspect <plugin_name>`,
  98. Args: cobra.ExactArgs(1),
  99. DisableAutoGenTag: true,
  100. RunE: func(cmd *cobra.Command, arg []string) error {
  101. var (
  102. cfg NotificationsCfg
  103. ok bool
  104. )
  105. pluginName := arg[0]
  106. if pluginName == "" {
  107. errors.New("Please provide a plugin name to inspect")
  108. }
  109. ncfgs, err := getNotificationsConfiguration()
  110. if err != nil {
  111. return errors.Wrap(err, "Can't build profiles configuration")
  112. }
  113. if cfg, ok = ncfgs[pluginName]; !ok {
  114. return errors.New("The provided plugin name doesn't exist or isn't active")
  115. }
  116. if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" {
  117. fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type)
  118. fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name)
  119. fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut)
  120. fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format)
  121. for k, v := range cfg.Config.Config {
  122. fmt.Printf(" - %15s: %15v\n", k, v)
  123. }
  124. } else if csConfig.Cscli.Output == "json" {
  125. x, err := json.MarshalIndent(cfg, "", " ")
  126. if err != nil {
  127. return errors.New("failed to marshal notification configuration")
  128. }
  129. fmt.Printf("%s", string(x))
  130. }
  131. return nil
  132. },
  133. }
  134. cmdNotifications.AddCommand(cmdNotificationsInspect)
  135. var remediation bool
  136. var alertOverride string
  137. var cmdNotificationsReinject = &cobra.Command{
  138. Use: "reinject",
  139. Short: "reinject alert into notifications system",
  140. Long: `Reinject alert into notifications system`,
  141. Example: `
  142. cscli notifications reinject <alert_id>
  143. cscli notifications reinject <alert_id> --remediation
  144. cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}'
  145. `,
  146. Args: cobra.ExactArgs(1),
  147. DisableAutoGenTag: true,
  148. RunE: func(cmd *cobra.Command, args []string) error {
  149. var (
  150. pluginBroker csplugin.PluginBroker
  151. pluginTomb tomb.Tomb
  152. )
  153. if len(args) != 1 {
  154. printHelp(cmd)
  155. return errors.New("Wrong number of argument: there should be one argument")
  156. }
  157. //first: get the alert
  158. id, err := strconv.Atoi(args[0])
  159. if err != nil {
  160. return errors.New(fmt.Sprintf("bad alert id %s", args[0]))
  161. }
  162. if err := csConfig.LoadAPIClient(); err != nil {
  163. return errors.Wrapf(err, "loading api client")
  164. }
  165. if csConfig.API.Client == nil {
  166. return errors.New("There is no configuration on 'api_client:'")
  167. }
  168. if csConfig.API.Client.Credentials == nil {
  169. return errors.New(fmt.Sprintf("Please provide credentials for the API in '%s'", csConfig.API.Client.CredentialsFilePath))
  170. }
  171. apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
  172. if err != nil {
  173. return errors.Wrapf(err, "error parsing the URL of the API")
  174. }
  175. client, err := apiclient.NewClient(&apiclient.Config{
  176. MachineID: csConfig.API.Client.Credentials.Login,
  177. Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
  178. UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
  179. URL: apiURL,
  180. VersionPrefix: "v1",
  181. })
  182. if err != nil {
  183. return errors.Wrapf(err, "error creating the client for the API")
  184. }
  185. alert, _, err := client.Alerts.GetByID(context.Background(), id)
  186. if err != nil {
  187. return errors.Wrapf(err, fmt.Sprintf("can't find alert with id %s", args[0]))
  188. }
  189. if alertOverride != "" {
  190. if err = json.Unmarshal([]byte(alertOverride), alert); err != nil {
  191. return errors.Wrapf(err, "Can't unmarshal the data given in the alert flag")
  192. }
  193. }
  194. if !remediation {
  195. alert.Remediation = true
  196. }
  197. // second we start plugins
  198. err = pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
  199. if err != nil {
  200. return errors.Wrapf(err, "Can't initialize plugins")
  201. }
  202. pluginTomb.Go(func() error {
  203. pluginBroker.Run(&pluginTomb)
  204. return nil
  205. })
  206. //third: get the profile(s), and process the whole stuff
  207. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  208. if err != nil {
  209. return errors.Wrap(err, "Cannot extract profiles from configuration")
  210. }
  211. for id, profile := range profiles {
  212. _, matched, err := profile.EvaluateProfile(alert)
  213. if err != nil {
  214. return errors.Wrapf(err, "can't evaluate profile %s", profile.Cfg.Name)
  215. }
  216. if !matched {
  217. log.Infof("The profile %s didn't match", profile.Cfg.Name)
  218. continue
  219. }
  220. log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name)
  221. loop:
  222. for {
  223. select {
  224. case pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  225. ProfileID: uint(id),
  226. Alert: alert,
  227. }:
  228. break loop
  229. default:
  230. time.Sleep(50 * time.Millisecond)
  231. log.Info("sleeping\n")
  232. }
  233. }
  234. if profile.Cfg.OnSuccess == "break" {
  235. log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
  236. break
  237. }
  238. }
  239. // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  240. pluginTomb.Kill(errors.New("terminating"))
  241. pluginTomb.Wait()
  242. return nil
  243. },
  244. }
  245. cmdNotificationsReinject.Flags().BoolVarP(&remediation, "remediation", "r", false, "Set Alert.Remediation to false in the reinjected alert (see your profile filter configuration)")
  246. cmdNotificationsReinject.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)")
  247. cmdNotifications.AddCommand(cmdNotificationsReinject)
  248. return cmdNotifications
  249. }
  250. func getNotificationsConfiguration() (map[string]NotificationsCfg, error) {
  251. pcfgs := map[string]csplugin.PluginConfig{}
  252. wf := func(path string, info fs.FileInfo, err error) error {
  253. if info == nil {
  254. return errors.Wrapf(err, "error while traversing directory %s", path)
  255. }
  256. name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
  257. if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
  258. ts, err := csplugin.ParsePluginConfigFile(name)
  259. if err != nil {
  260. return errors.Wrapf(err, "Loading notifification plugin configuration with %s", name)
  261. }
  262. for _, t := range ts {
  263. pcfgs[t.Name] = t
  264. }
  265. }
  266. return nil
  267. }
  268. if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil {
  269. return nil, errors.Wrap(err, "Loading notifification plugin configuration")
  270. }
  271. // A bit of a tricky stuf now: reconcile profiles and notification plugins
  272. ncfgs := map[string]NotificationsCfg{}
  273. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  274. if err != nil {
  275. return nil, errors.Wrap(err, "Cannot extract profiles from configuration")
  276. }
  277. for profileID, profile := range profiles {
  278. loop:
  279. for _, notif := range profile.Cfg.Notifications {
  280. for name, pc := range pcfgs {
  281. if notif == name {
  282. if _, ok := ncfgs[pc.Name]; !ok {
  283. ncfgs[pc.Name] = NotificationsCfg{
  284. Config: pc,
  285. Profiles: []*csconfig.ProfileCfg{profile.Cfg},
  286. ids: []uint{uint(profileID)},
  287. }
  288. continue loop
  289. }
  290. tmp := ncfgs[pc.Name]
  291. for _, pr := range tmp.Profiles {
  292. var profiles []*csconfig.ProfileCfg
  293. if pr.Name == profile.Cfg.Name {
  294. continue
  295. }
  296. profiles = append(tmp.Profiles, profile.Cfg)
  297. ids := append(tmp.ids, uint(profileID))
  298. ncfgs[pc.Name] = NotificationsCfg{
  299. Config: tmp.Config,
  300. Profiles: profiles,
  301. ids: ids,
  302. }
  303. }
  304. }
  305. }
  306. }
  307. }
  308. return ncfgs, nil
  309. }